├── .gitignore ├── LICENSE ├── README.md ├── basic-interaction-wallet-inplug-v2.ts ├── build.ts ├── dist ├── wallet-da.fif ├── wallet.boc └── wallet.fif ├── env.ts ├── func ├── .plugin-jetton-control.fc ├── plugin-seed-in.fc ├── plugin.fc ├── stdlib.fc └── wallet.fc ├── package-lock.json ├── package.json ├── test-wallet-inplug-v2.ts └── wallet-inplug-v2.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | env.ts 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ProgramCrafter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wallet Inplug-v1 2 | Wallet Inplug-v1 (previously known as v5) is custom version of wallet for use instead of v4. 3 | 4 | There are three main differences from previous versions: 5 | 1. execute arbitrary code onchain. User can pass a continuation to be executed -- this is useful for predicting unsafe random. 6 | 2. plugin functionality: plugins are continuations, like small inlined contracts. They may be executed upon getting an incoming message. 7 | Actual plugin's code is stored in masterchain, so that plugins are just masterchain libraries. 8 | 3. secp256r1 keys are supported in addition to Ed25519 ones. 9 | 10 | All plugin functions must be inlined or `inline_ref`ed, as otherwise call will go out of plugin control so the function won't be found. 11 | 12 | # Considerations 13 | 14 | - [x] Making all functions inline is not the optimal way if you call them in different parts of code. 15 | - Mentioned that `inline_ref` is also working. Putting functions in cell references incurs no significant overhead and allows to use them in several places. 16 | - [x] Plugins are considered to be trusted and called without isolation but they should not be trusted. 17 | - Verifying that plugin outgoing messages are valid is expensive and involves pretty large number of cases. Thus, I think that public plugins should come with formal proof of properties checkable by wallet application. 18 | - [x] Continuations in signed messages are considered to be trusted and called without isolation but they should not be trusted. 19 | - In presence of isolation, calling code onchain does not make much sense except for forming large software-generated messages. No isolation allows to use wallet v5 for more exotic usecases, such as changing public key. 20 | - The fact whether execution of some code breaks the wallet should be determined offchain before signing message. 21 | - [x] Timestamp of the signed message does not have upper limit, delayed message attack is possible. 22 | - Delayed messages may be useful for prolonging domain contracts. 23 | - If wallet application signs the message, nothing saves it from setting the wrong upper limit timestamp as well. 24 | - If a delayed signed message is recorded, it is possible to change seqno (or public key, though it's more complicated way) to invalidate that message. 25 | - [ ] Plugins should be tested for reentrancy issues just like Ethereum smart contracts. 26 | -------------------------------------------------------------------------------- /basic-interaction-wallet-inplug-v2.ts: -------------------------------------------------------------------------------- 1 | import { Address, Cell, toNano, TonClient } from "@ton/ton"; 2 | import { getHttpEndpoint } from "@orbs-network/ton-access"; 3 | import { mnemonicToPrivateKey } from "@ton/crypto"; 4 | import yargs from "yargs"; 5 | 6 | import { makeSender, WalletInplugV2 } from "./wallet-inplug-v2"; 7 | import { mnemonic } from "./env"; 8 | 9 | function expect(a: any) { 10 | return { 11 | toBe: function(b: any) { 12 | if (a === b) return; 13 | throw new Error("mismatch between " + a + " " + b); 14 | } 15 | } 16 | } 17 | 18 | const argv = yargs 19 | .option('action', { 20 | alias: 'a', 21 | description: 'The action to perform (info|transfer|install|uninstall|execute)', 22 | type: 'string', 23 | demandOption: true, 24 | }) 25 | .option('dest', { 26 | alias: 'd', 27 | description: 'The destination address', 28 | type: 'string', 29 | demandOption: false, 30 | }) 31 | .option('value', { 32 | alias: 'v', 33 | description: 'The value to transfer', 34 | type: 'string', 35 | demandOption: false, 36 | }) 37 | .option('body', { 38 | alias: 'b', 39 | description: 'Plugin storage/optional body data', 40 | type: 'string', 41 | demandOption: false, 42 | }) 43 | .option('code', { 44 | alias: 'c', 45 | description: 'Invocable/plugin code', 46 | type: 'string', 47 | demandOption: false, 48 | }).argv; 49 | 50 | (async () => { 51 | const args = await argv; 52 | 53 | const endpoint = await getHttpEndpoint({network: 'testnet'}); 54 | const client = new TonClient({ endpoint }); 55 | 56 | const key = await mnemonicToPrivateKey(mnemonic.split(' ')); 57 | const wallet = client.open(await WalletInplugV2.create({workchain: 0, publicKey: key.publicKey})); 58 | console.log(wallet.address); 59 | console.log('TON: ', await wallet.getBalance()); 60 | console.log('Seqno:', await wallet.getSeqno()); 61 | 62 | if (args.action == 'info') { 63 | return; 64 | } else if (args.action == 'transfer') { 65 | const destination = Address.parse(args.dest!!); 66 | const value = toNano(args.value!!); 67 | 68 | const body = args.body ? Cell.fromBase64(args.body) : undefined; 69 | 70 | console.log(await makeSender(wallet as any, key.secretKey).send({ 71 | to: destination, 72 | value, 73 | body 74 | })); 75 | } else if (args.action == 'install') { 76 | const pluginCode = Cell.fromBase64(args.code!); 77 | const pluginData = args.body ? Cell.fromBase64(args.body!) : Cell.EMPTY; 78 | await wallet.sendInstallPlugin({secretKey: key.secretKey, code: pluginCode, data: pluginData}); 79 | } else if (args.action == 'uninstall') { 80 | const pluginCode = Cell.fromBase64(args.code!); 81 | const pluginData = args.body ? Cell.fromBase64(args.body!) : Cell.EMPTY; 82 | await wallet.sendUninstallPlugin({secretKey: key.secretKey, code: pluginCode, data: pluginData}); 83 | } else if (args.action == 'execute') { 84 | const code = Cell.fromBase64(args.code!); 85 | await wallet.sendExecuteCode({secretKey: key.secretKey, code}); 86 | } else { 87 | console.warn('Unsupported operation'); 88 | } 89 | })(); 90 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import {compileFunc, compilerVersion} from '@ton-community/func-js'; 2 | import {readFile, writeFile} from 'fs/promises'; 3 | import {fromCode} from 'tvm-disassembler'; 4 | import {Cell} from '@ton/core'; 5 | 6 | export async function disasm(c: Cell): Promise { 7 | let s = await fromCode(c); 8 | return s.replaceAll('s0 s1 XCHG', 'SWAP') 9 | .replaceAll('s0 PUSH', 'DUP'); 10 | } 11 | 12 | export async function compile(outFile: boolean = true, postprocess: boolean = true) { 13 | console.log(await compilerVersion()); 14 | 15 | let result = await compileFunc({ 16 | targets: ['stdlib.fc', 'wallet.fc'], 17 | sources: { 18 | 'stdlib.fc': await readFile('./func/stdlib.fc', {encoding: 'utf-8'}), 19 | 'wallet.fc': await readFile('./func/wallet.fc', {encoding: 'utf-8'}), 20 | } 21 | }); 22 | if (result.status === 'error') { 23 | console.error(result.message) 24 | return null; 25 | } 26 | if (result.warnings) 27 | console.warn(result.warnings); 28 | 29 | if (outFile) { 30 | await writeFile('./dist/wallet.fif', result.fiftCode); 31 | await writeFile('./dist/wallet.boc', result.codeBoc); 32 | if (postprocess) 33 | await writeFile('./dist/wallet-da.fif', await disasm(Cell.fromBase64(result.codeBoc))); 34 | } 35 | return result.codeBoc; 36 | } 37 | 38 | export async function compilePlugin() { 39 | let result = await compileFunc({ 40 | targets: ['stdlib.fc', 'plugin.fc'], 41 | sources: { 42 | 'stdlib.fc': await readFile('./func/stdlib.fc', {encoding: 'utf-8'}), 43 | 'plugin.fc': await readFile('./func/plugin.fc', {encoding: 'utf-8'}), 44 | } 45 | }); 46 | if (result.status === 'ok') return result.codeBoc; 47 | return null; 48 | } 49 | 50 | export const walletBoc = compile(); 51 | -------------------------------------------------------------------------------- /dist/wallet-da.fif: -------------------------------------------------------------------------------- 1 | SETCP0 2 | (:methods 3 | recv_internal: 4 | DUP 5 | 64 PLDUZ 6 | 1154835125 PUSHINT 7 | NEQ 8 | IFRETALT 9 | 32 PUSHINT 10 | SDSKIPFIRST 11 | 256 LDU 12 | c4 PUSH 13 | CTOS 14 | 64 LDU 15 | LDDICT 16 | s1 s2 XCHG 17 | 0 PUSHINT 18 | s0 s4 s5 XCHG3 19 | s0 s3 PUSH2 20 | 8 PUSHPOW2 21 | DICTUGETREF 22 | NULLSWAPIFNOT 23 | 128 THROWIFNOT 24 | 3 2 3 XC2PU 25 | <{ 26 | s3 PUSH 27 | CTOS 28 | 1 LDU 29 | SWAP 30 | <{ 31 | LDREF 32 | 0 PLDREFIDX 33 | DUP 34 | HASHCU 35 | s2 PUSH 36 | CTOS 37 | BLESS 38 | s0 s6 XCHG 39 | 32105 PUSHINT 40 | MUL 41 | 121535 PUSHINT 42 | ADD 43 | s2 s3 XCHG 44 | s5 s0 s6 XCHG3 45 | 4 1 CALLXARGS 46 | DUP 47 | HASHCU 48 | s0 s3 XCHG2 49 | EQUAL 50 | <{ 51 | 2DROP 52 | }> PUSHCONT 53 | IFJMP 54 | 1 2 BLKDROP2 55 | NEWC 56 | STDICT 57 | STREF 58 | ENDC 59 | }> PUSHCONT 60 | IFJMP 61 | s4 POP 62 | s0 s3 XCHG 63 | 33 LDU 64 | LDREF 65 | LDREF 66 | 32 LDU 67 | NEWC 68 | s5 s2 PUSH2 69 | ADD 70 | SWAP 71 | 33 STU 72 | s4 s-1 PUXC 73 | STREF 74 | s1 s2 XCHG 75 | 32 STU 76 | s1 PUSH 77 | STSLICER 78 | s0 s6 XCHG 79 | 63 THROWIF 80 | NOW 81 | s0 s4 XCHG2 82 | LESS 83 | 62 THROWIF 84 | SWAP 85 | CTOS 86 | BLESS 87 | s3 s5 XCHG 88 | s1 s5 s0 XCHG3 89 | 121535 PUSHINT 90 | SWAP 91 | 4 PUSHINT 92 | SWAP 93 | 1 PUSHINT 94 | s5 PUSH 95 | 293 RUNVM 96 | 1 1 BLKDROP2 97 | SWAP 98 | 1 GTINT 99 | 61 THROWIF 100 | s0 s2 XCHG 101 | STREF 102 | ENDC 103 | s1 PUSH 104 | CTOS 105 | LDREF 106 | SWAP 107 | HASHCU 108 | 68134197439415885698044414435951397869210496020759160419881882418413283430343 PUSHINT 109 | EQUAL 110 | 60 THROWIFNOT 111 | 32 LDU 112 | SWAP 113 | 247711853 PUSHINT 114 | EQUAL 115 | 60 THROWIFNOT 116 | 8 LDU 117 | SWAP 118 | 188 PUSHINT 119 | AND 120 | 60 THROWIF 121 | LDREF 122 | ENDS 123 | CTOS 124 | 4 LDU 125 | SWAP 126 | 5 PUSHINT 127 | AND 128 | 4 EQINT 129 | 59 THROWIFNOT 130 | LDMSGADDR 131 | s1 POP 132 | LDMSGADDR 133 | s1 POP 134 | LDGRAMS 135 | SKIPDICT 136 | LDGRAMS 137 | s1 POP 138 | LDGRAMS 139 | s1 POP 140 | 97 LDU 141 | SWAP 142 | 1 PUSHINT 143 | AND 144 | 59 THROWIF 145 | 1 LDU 146 | SWAP 147 | <{ 148 | LDREF 149 | ENDS 150 | CTOS 151 | }> PUSHCONT 152 | IF 153 | 64 PLDUZ 154 | s0 s4 XCHG 155 | LDDICT 156 | s5 s1 PUXC 157 | 32 PUSHINT 158 | DICTUGET 159 | NULLSWAPIFNOT 160 | s1 POP 161 | s0 s5 XCHG 162 | 0 EQINT 163 | s1 s5 XCHG 164 | OR 165 | 58 THROWIFNOT 166 | s0 s3 XCHG 167 | LDGRAMS 168 | s0 POP 169 | s1 s3 XCHG 170 | LEQ 171 | 58 THROWIFNOT 172 | c5 POP 173 | }> CALLREF 174 | s0 s2 XCHG 175 | 8 PUSHPOW2 176 | DICTUSETREF 177 | ROT 178 | NEWC 179 | 64 STU 180 | STDICT 181 | SWAP 182 | STSLICER 183 | ENDC 184 | c4 POP 185 | 186 | is_plugin_installed: 187 | c4 PUSH 188 | CTOS 189 | 64 LDU 190 | LDDICT 191 | s0 POP 192 | s1 POP 193 | 8 PUSHPOW2 194 | DICTUGET 195 | NULLSWAPIFNOT 196 | s1 POP 197 | 198 | get_public_key: 199 | c4 PUSH 200 | CTOS 201 | 8 PUSHPOW2 202 | SDCUTLAST 203 | 204 | seqno: 205 | c4 PUSH 206 | CTOS 207 | 64 PLDU 208 | 209 | recv_external: 210 | 32 LDU 211 | s1 PUSH 212 | 338459908 PUSHINT 213 | EQUAL 214 | <{ 215 | SWAP 216 | 1154835125 PUSHINT 217 | EQUAL 218 | <{ 219 | c4 PUSH 220 | CTOS 221 | 64 LDU 222 | LDDICT 223 | s0 s3 XCHG 224 | 256 LDU 225 | -1 PUSHINT 226 | s0 s2 XCHG 227 | s0 s3 PUSH2 228 | 8 PUSHPOW2 229 | DICTUGETREF 230 | NULLSWAPIFNOT 231 | 128 THROWIFNOT 232 | 3 2 3 XC2PU 233 | <{ 234 | s3 PUSH 235 | CTOS 236 | 1 LDU 237 | SWAP 238 | <{ 239 | LDREF 240 | 0 PLDREFIDX 241 | DUP 242 | HASHCU 243 | s2 PUSH 244 | CTOS 245 | BLESS 246 | s0 s6 XCHG 247 | 32105 PUSHINT 248 | MUL 249 | 121535 PUSHINT 250 | ADD 251 | s2 s3 XCHG 252 | s5 s0 s6 XCHG3 253 | 4 1 CALLXARGS 254 | DUP 255 | HASHCU 256 | s0 s3 XCHG2 257 | EQUAL 258 | <{ 259 | 2DROP 260 | }> PUSHCONT 261 | IFJMP 262 | 1 2 BLKDROP2 263 | NEWC 264 | STDICT 265 | STREF 266 | ENDC 267 | }> PUSHCONT 268 | IFJMP 269 | s4 POP 270 | s0 s3 XCHG 271 | 33 LDU 272 | LDREF 273 | LDREF 274 | 32 LDU 275 | NEWC 276 | s5 s2 PUSH2 277 | ADD 278 | SWAP 279 | 33 STU 280 | s4 s-1 PUXC 281 | STREF 282 | s1 s2 XCHG 283 | 32 STU 284 | s1 PUSH 285 | STSLICER 286 | s0 s6 XCHG 287 | 63 THROWIF 288 | NOW 289 | s0 s4 XCHG2 290 | LESS 291 | 62 THROWIF 292 | SWAP 293 | CTOS 294 | BLESS 295 | s3 s5 XCHG 296 | s1 s5 s0 XCHG3 297 | 121535 PUSHINT 298 | SWAP 299 | 4 PUSHINT 300 | SWAP 301 | 1 PUSHINT 302 | s5 PUSH 303 | 293 RUNVM 304 | 1 1 BLKDROP2 305 | SWAP 306 | 1 GTINT 307 | 61 THROWIF 308 | s0 s2 XCHG 309 | STREF 310 | ENDC 311 | s1 PUSH 312 | CTOS 313 | LDREF 314 | SWAP 315 | HASHCU 316 | 68134197439415885698044414435951397869210496020759160419881882418413283430343 PUSHINT 317 | EQUAL 318 | 60 THROWIFNOT 319 | 32 LDU 320 | SWAP 321 | 247711853 PUSHINT 322 | EQUAL 323 | 60 THROWIFNOT 324 | 8 LDU 325 | SWAP 326 | 188 PUSHINT 327 | AND 328 | 60 THROWIF 329 | LDREF 330 | ENDS 331 | CTOS 332 | 4 LDU 333 | SWAP 334 | 5 PUSHINT 335 | AND 336 | 4 EQINT 337 | 59 THROWIFNOT 338 | LDMSGADDR 339 | s1 POP 340 | LDMSGADDR 341 | s1 POP 342 | LDGRAMS 343 | SKIPDICT 344 | LDGRAMS 345 | s1 POP 346 | LDGRAMS 347 | s1 POP 348 | 97 LDU 349 | SWAP 350 | 1 PUSHINT 351 | AND 352 | 59 THROWIF 353 | 1 LDU 354 | SWAP 355 | <{ 356 | LDREF 357 | ENDS 358 | CTOS 359 | }> PUSHCONT 360 | IF 361 | 64 PLDUZ 362 | s0 s4 XCHG 363 | LDDICT 364 | s5 s1 PUXC 365 | 32 PUSHINT 366 | DICTUGET 367 | NULLSWAPIFNOT 368 | s1 POP 369 | s0 s5 XCHG 370 | 0 EQINT 371 | s1 s5 XCHG 372 | OR 373 | 58 THROWIFNOT 374 | s0 s3 XCHG 375 | LDGRAMS 376 | s0 POP 377 | s1 s3 XCHG 378 | LEQ 379 | 58 THROWIFNOT 380 | c5 POP 381 | }> CALLREF 382 | s0 s2 XCHG 383 | 8 PUSHPOW2 384 | DICTUSETREF 385 | SWAP 386 | NEWC 387 | 64 STU 388 | STDICT 389 | SWAP 390 | STSLICER 391 | ENDC 392 | c4 POP 393 | }> PUSHCONT 394 | <{ 395 | s0 POP 396 | }> PUSHCONT 397 | IFELSE 398 | }> PUSHCONT 399 | <{ 400 | s1 POP 401 | c4 PUSH 402 | CTOS 403 | 64 LDU 404 | LDDICT 405 | s3 s3 XCPU 406 | SWAP 407 | LDREF 408 | s1 PUSH 409 | HASHCU 410 | s0 s3 XCHG 411 | 256 PLDU 412 | s2 s3 XCHG 413 | CHKSIGNU 414 | 310 THROWIFNOT 415 | CTOS 416 | 64 LDU 417 | s1 s3 XCPU 418 | EQUAL 419 | 312 THROWIFNOT 420 | 32 LDU 421 | SWAP 422 | NOW 423 | GEQ 424 | 313 THROWIFNOT 425 | s2 PUSH 426 | INC 427 | 4 2 -2 PU2XC 428 | NEWC 429 | 64 STU 430 | STDICT 431 | SWAP 432 | STSLICER 433 | ENDC 434 | c4 POP 435 | COMMIT 436 | ACCEPT 437 | LDDICT 438 | s1 PUSH 439 | ISNULL 440 | NOT 441 | <{ 442 | SWAP 443 | c5 POP 444 | }> PUSHCONT 445 | <{ 446 | s1 POP 447 | }> PUSHCONT 448 | IFELSE 449 | PLDDICT 450 | <{ 451 | DUP 452 | ISNULL 453 | NOT 454 | }> PUSHCONT 455 | PUSHREFCONT 456 | WHILE 457 | s0 POP 458 | SWAP 459 | INC 460 | NEWC 461 | 64 STU 462 | STDICT 463 | SWAP 464 | STSLICER 465 | ENDC 466 | c4 POP 467 | CTOS 468 | 2 LDU 469 | s1 PUSH 470 | 0 EQINT 471 | <{ 472 | s1 POP 473 | DUP 474 | 256 PLDU 475 | s1 s2 XCHG 476 | SWAP 477 | 8 PUSHPOW2 478 | DICTUDEL 479 | s0 POP 480 | }> PUSHCONT 481 | <{ 482 | s1 PUSH 483 | 1 EQINT 484 | <{ 485 | s1 POP 486 | LDREF 487 | ROTREV 488 | 8 PUSHPOW2 489 | s1 PUSH 490 | HASHCU 491 | s0 s3 s3 XCHG3 492 | DICTUSETREF 493 | }> PUSHCONT 494 | <{ 495 | SWAP 496 | 3 EQINT 497 | <{ 498 | LDREFRTOS 499 | BLESS 500 | 0 0 CALLXARGS 501 | }> PUSHCONT 502 | <{ 503 | 303 THROW 504 | }> PUSHCONT 505 | IFELSE 506 | SWAP 507 | }> PUSHCONT 508 | IFELSE 509 | }> PUSHCONT 510 | IFELSE 511 | SWAP 512 | PLDDICT 513 | }> IFREFELSE 514 | ) 19 DICTPUSHCONST 515 | DICTIGETJMPZ 516 | 11 THROWARG 517 | -------------------------------------------------------------------------------- /dist/wallet.boc: -------------------------------------------------------------------------------- 1 | te6ccgECDwEAAoEAART/APSkE/S88sgLAQIBIAIDAgFIBAUCkvLTHyGCEBQsfQS6jroBghBE1WK1uo6t7UTQ0z/0BAPT/38CUwODB/QPb6Hy4IBUEyPbPAKDB/QXAcjLP/QAAc8Wye1UkTDi4w0KCwF+0CDXEIIQRNVitb3jCIAg1yHT/+1E0NM/9AQScEBFUwODB/QPb6Hy4IBUEyPbPAKDB/QXWMjLP/QAAc8Wye1UCgIBSAYHAgFYCAkAEbjJftRNDXCz+AAlsp37UTQ0z/0BDAxgwf0Dm+hMYAATsOc7UTQgwfXIoAHwI9DTAAGOKtTXTCD5ACLQ7R4GgX1pqIIB2r+gECNFBtpBIPkAUAO6kVvgbBLI9ADMyeA0A9Mg1NTTH8hTUqAByyBSQMwSyx8hzxYG8n/4I1AEufJ+AdDtHhA1QVCCAdq/AXQBcSXbQSVsEQHCAfJ9AszJIdDUAfkADAHEMe1E0NM/9ARRMwHUIfkAA9cL/xAj+RDy4TbQ0z9RE7ry4TjTHwH4I77y4TkipFRkMMjLP/QAAc8Wye1U+A/4APQEIW6zkwHtVZEx4vQFkyBus4roMAGkyMs/9AABzxbJ7VQOAf6C8JailtIk8oXGe+6Tww+KMJFX8NqjXcW4fkELeGMKCc/HuvK80x8BghAOw8htuvK80wcBgQC8sPJ81NHQ0wMBdbDABPK7+kAx+kAx+gD0AfoAMfoAMdNgAXGw8nvTAAGT1NHQ3tcQBPQEUlKAIPQOb6ExBcAAFbHyugP6ADATDQAKu/K67VUAdNDTASHAAJwxINcL/xIBgwf0WzCOISHAAZwx1FmDByH5AEAz9BefAcADldXtHtoAk/LBL+IB4uIB9AU= -------------------------------------------------------------------------------- /dist/wallet.fif: -------------------------------------------------------------------------------- 1 | // automatically generated from `stdlib.fc` `wallet.fc` 2 | PROGRAM{ 3 | DECLPROC load_contract 4 | DECLPROC save_contract 5 | 85143 DECLMETHOD seqno 6 | 78748 DECLMETHOD get_public_key 7 | 76407 DECLMETHOD is_plugin_installed 8 | DECLPROC check_any_signature 9 | DECLPROC state_execute_plugin 10 | DECLPROC execute_plugin 11 | DECLPROC install_plugin 12 | DECLPROC uninstall_plugin 13 | DECLPROC ~do 14 | DECLPROC main 15 | DECLPROC recv_external 16 | load_contract PROCINLINE:<{ 17 | c4 PUSH 18 | CTOS 19 | 64 LDU 20 | LDDICT 21 | }> 22 | save_contract PROCINLINE:<{ 23 | NEWC 24 | 64 STU 25 | STDICT 26 | SWAP 27 | STSLICER 28 | ENDC 29 | c4 POP 30 | }> 31 | seqno PROC:<{ 32 | c4 PUSH 33 | CTOS 34 | 64 PLDU 35 | }> 36 | get_public_key PROC:<{ 37 | c4 PUSH 38 | CTOS 39 | 8 PUSHPOW2 40 | SDCUTLAST 41 | }> 42 | is_plugin_installed PROC:<{ 43 | load_contract INLINECALLDICT 44 | DROP 45 | NIP 46 | 8 PUSHPOW2 47 | DICTUGET 48 | NULLSWAPIFNOT 49 | NIP 50 | }> 51 | check_any_signature PROCINLINE:<{ 52 | SWAP 53 | LDREF 54 | OVER 55 | HASHCU 56 | s0 s3 XCHG 57 | 256 PLDU 58 | s2 s3 XCHG 59 | CHKSIGNU 60 | 310 THROWIFNOT 61 | }> 62 | state_execute_plugin PROCREF:<{ 63 | s3 PUSH 64 | CTOS 65 | 1 LDU 66 | SWAP 67 | IFJMP:<{ 68 | LDREF 69 | PLDREF 70 | DUP 71 | HASHCU 72 | s2 PUSH 73 | CTOS 74 | BLESS 75 | s0 s6 XCHG 76 | 32105 PUSHINT 77 | MUL 78 | 121535 PUSHINT 79 | ADD 80 | s2 s3 XCHG 81 | s5 s0 s6 XCHG3 82 | 4 1 CALLXARGS 83 | DUP 84 | HASHCU 85 | s0 s3 XCHG2 86 | EQUAL 87 | IFJMP:<{ 88 | 2DROP 89 | }> 90 | 1 2 BLKDROP2 91 | NEWC 92 | STDICT 93 | STREF 94 | ENDC 95 | }> 96 | s4 POP 97 | s0 s3 XCHG 98 | 33 LDU 99 | LDREF 100 | LDREF 101 | 32 LDU 102 | NEWC 103 | s5 s2 PUSH2 104 | ADD 105 | SWAP 106 | 33 STU 107 | s4 s(-1) PUXC 108 | STREF 109 | s1 s2 XCHG 110 | 32 STU 111 | OVER 112 | STSLICER 113 | s0 s6 XCHG 114 | 63 THROWIF 115 | NOW 116 | s0 s4 XCHG2 117 | LESS 118 | 62 THROWIF 119 | SWAP 120 | CTOS 121 | BLESS 122 | s3 s5 XCHG 123 | s1 s5 s0 XCHG3 124 | 121535 PUSHINT 125 | SWAP 126 | 4 PUSHINT SWAP ONE // addr data msg method in=4 f out=1 127 | s5 PUSH // addr data msg method in=4 f out=1 c4=data 128 | // (293 sets c3=f, c4=data, returns c4', c5 and one value from stack) 129 | 293 RUNVM // data' exitcode c4' c5 130 | 1 1 BLKDROP2 // data' exitcode c5 131 | SWAP 132 | 1 GTINT 133 | 61 THROWIF 134 | s0 s2 XCHG 135 | STREF 136 | ENDC 137 | OVER 138 | CTOS 139 | LDREF 140 | SWAP 141 | HASHCU 142 | 68134197439415885698044414435951397869210496020759160419881882418413283430343 PUSHINT 143 | EQUAL 144 | 60 THROWIFNOT 145 | 32 LDU 146 | SWAP 147 | 247711853 PUSHINT 148 | EQUAL 149 | 60 THROWIFNOT 150 | 8 LDU 151 | SWAP 152 | 188 PUSHINT 153 | AND 154 | 60 THROWIF 155 | LDREF 156 | ENDS 157 | CTOS 158 | 4 LDU 159 | SWAP 160 | 5 PUSHINT 161 | AND 162 | 4 EQINT 163 | 59 THROWIFNOT 164 | LDMSGADDR 165 | NIP 166 | LDMSGADDR 167 | NIP 168 | LDGRAMS 169 | SKIPDICT 170 | LDGRAMS 171 | NIP 172 | LDGRAMS 173 | NIP 174 | 97 LDU 175 | SWAP 176 | 1 PUSHINT 177 | AND 178 | 59 THROWIF 179 | 1 LDU 180 | SWAP 181 | IF:<{ 182 | LDREF ENDS CTOS 183 | }> 184 | 32 PLDUZ 185 | s0 s4 XCHG 186 | LDDICT 187 | s5 s1 PUXC 188 | 32 PUSHINT 189 | DICTUGET 190 | NULLSWAPIFNOT 191 | NIP 192 | s0 s5 XCHG 193 | 0 EQINT 194 | s1 s5 XCHG 195 | OR 196 | 58 THROWIFNOT 197 | s0 s3 XCHG 198 | LDGRAMS 199 | DROP 200 | s1 s3 XCHG 201 | LEQ 202 | 58 THROWIFNOT 203 | c5 POPCTR 204 | }> 205 | execute_plugin PROCINLINE:<{ 206 | s0 s3 PUSH2 207 | 8 PUSHPOW2 208 | DICTUGETREF 209 | NULLSWAPIFNOT 210 | 128 THROWIFNOT 211 | s3 s2 s3 XC2PU 212 | state_execute_plugin INLINECALLDICT 213 | s0 s2 XCHG 214 | 8 PUSHPOW2 215 | DICTUSETREF 216 | }> 217 | install_plugin PROCINLINE:<{ 218 | 8 PUSHPOW2 219 | OVER 220 | HASHCU 221 | s0 s3 s3 XCHG3 222 | DICTUSETREF 223 | }> 224 | uninstall_plugin PROCINLINE:<{ 225 | SWAP 226 | 8 PUSHPOW2 227 | DICTUDEL 228 | }> 229 | ~do PROCINLINE:<{ 230 | CTOS 231 | 2 LDU 232 | OVER 233 | 0 EQINT 234 | IF:<{ 235 | NIP 236 | DUP 237 | 256 PLDU 238 | s1 s2 XCHG 239 | uninstall_plugin INLINECALLDICT 240 | DROP 241 | }>ELSE<{ 242 | OVER 243 | 1 EQINT 244 | IF:<{ 245 | NIP 246 | LDREF 247 | -ROT 248 | install_plugin INLINECALLDICT 249 | }>ELSE<{ 250 | SWAP 251 | 3 EQINT 252 | IF:<{ 253 | LDREFRTOS 254 | BLESS 255 | 0 0 CALLXARGS 256 | }>ELSE<{ 257 | 303 THROW 258 | }> 259 | SWAP 260 | }> 261 | }> 262 | SWAP 263 | PLDOPTREF 264 | }> 265 | main PROC:<{ 266 | DUP 267 | 32 PLDUZ 268 | 1154835125 PUSHINT 269 | NEQ 270 | IFRETALT 271 | 32 PUSHINT 272 | SDSKIPFIRST 273 | 256 LDU 274 | load_contract INLINECALLDICT 275 | s1 s2 XCHG 276 | 0 PUSHINT 277 | s0 s4 s5 XCHG3 278 | execute_plugin INLINECALLDICT 279 | ROT 280 | save_contract INLINECALLDICT 281 | }> 282 | recv_external PROC:<{ 283 | 32 LDU 284 | OVER 285 | 338459908 PUSHINT 286 | EQUAL 287 | IF:<{ 288 | NIP 289 | load_contract INLINECALLDICT 290 | s3 s3 XCPU 291 | check_any_signature INLINECALLDICT 292 | CTOS 293 | 64 LDU 294 | s1 s3 XCPU 295 | EQUAL 296 | 312 THROWIFNOT 297 | 32 LDU 298 | SWAP 299 | NOW 300 | GEQ 301 | 313 THROWIFNOT 302 | s2 PUSH 303 | INC 304 | s4 s2 s(-2) PU2XC 305 | save_contract INLINECALLDICT 306 | COMMIT 307 | ACCEPT 308 | LDOPTREF 309 | OVER 310 | ISNULL 311 | NOT 312 | IF:<{ 313 | SWAP 314 | c5 POPCTR 315 | }>ELSE<{ 316 | NIP 317 | }> 318 | PLDOPTREF 319 | WHILE:<{ 320 | DUP 321 | ISNULL 322 | NOT 323 | }>DO<{ 324 | ~do INLINECALLDICT 325 | }> 326 | DROP 327 | SWAP 328 | INC 329 | save_contract INLINECALLDICT 330 | }>ELSE<{ 331 | SWAP 332 | 1154835125 PUSHINT 333 | EQUAL 334 | IF:<{ 335 | load_contract INLINECALLDICT 336 | s0 s3 XCHG 337 | 256 LDU 338 | -1 PUSHINT 339 | s0 s2 XCHG 340 | execute_plugin INLINECALLDICT 341 | SWAP 342 | save_contract INLINECALLDICT 343 | }>ELSE<{ 344 | DROP 345 | }> 346 | }> 347 | }> 348 | }END>c 349 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | export const mnemonic = 'apple '.repeat(24); 2 | -------------------------------------------------------------------------------- /func/.plugin-jetton-control.fc: -------------------------------------------------------------------------------- 1 | ;; Plugin to allow zero-gas jetton transfers from user's wallet, based on public key auth. 2 | ;; (c) ProgramCrafter, 2023, subject to MIT license (see the root directory of repository) 3 | ;; 4 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 5 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 6 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 7 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 8 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 9 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 10 | ;; SOFTWARE. 11 | ;; 12 | ;; The plugin is NOT ready for production use. 13 | 14 | 15 | 16 | ;; -------------------- DELEGATOR SIGNATURE CHECKING METHOD -------------------- 17 | 18 | int check_secp256r1(int signed, slice signature, slice key) asm "P256_CHKSIGNU"; 19 | 20 | cell check_any_signature(slice in_msg_body) impure inline { 21 | cell request = in_msg_body~load_ref(); 22 | int request_hash = request.cell_hash(); 23 | if (key_type == 0x0080de4f) { 24 | throw_unless(310, check_signature(request_hash, in_msg_body, key.preload_uint(256))); 25 | } 26 | 27 | return request; 28 | } 29 | 30 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 31 | 32 | () main() { 33 | throw(65535); 34 | } 35 | 36 | slice creator_address() method_id(119703) { 37 | return "EQCyoez1VF4HbNNq5Rbqfr3zKuoAjKorhK-YZr7LIIiVrSD7"a; 38 | } 39 | 40 | cell process_internal(int my_id, cell my_storage, int balance, int msg_value, cell wallet_in_msg, slice plugin_msg_body) method_id(121535) { 41 | ;; TODO: change infinite jetton allowance to something stored in storage 42 | ;; storage::jetton-control::full-allowance#c548e4c7 ecdsa_public:bits256 seqno:uint32 = Storage; 43 | 44 | ;; send-jettons#0e1f887c seqno:uint32 jetton_wallet:MsgAddressInt ton_total:coins jettons:coins destination:MsgAddressInt = ActionInfo; 45 | ;; message::jetton-control#_ signature_info:^bits512 info:^ActionInfo = InMsgBody; 46 | 47 | ;; loading storage 48 | throw_if(180, my_storage.cell_null?()); 49 | slice ds = my_storage.begin_parse(); 50 | throw_unless(181, ds~load_uint(32) == 0xc548e4c7); 51 | int public_key = ds~load_uint(256); 52 | int seqno = ds.preload_uint(32); 53 | 54 | ;; loading and verifying incoming message 55 | slice signature = plugin_msg_body~load_ref().begin_parse(); 56 | cell request = plugin_msg_body~load_ref(); 57 | throw_unless(310, check_signature(request.cell_hash(), signature, public_key)); 58 | slice request = request.begin_parse(); 59 | throw_unless(320, request~load_uint(32) == 0x0e1f887c); 60 | throw_unless(311, request~load_uint(32) == public_key); 61 | slice jetton_wallet = request~load_msg_addr(); 62 | int ton_total = request~load_coins(); 63 | int jettons = request~load_coins(); 64 | slice destination = request~load_msg_addr(); 65 | request.end_parse(); 66 | 67 | ;; sending outgoing messages 68 | send_raw_message(begin_cell() 69 | .store_uint(0x18, 6) 70 | .store_slice(jetton_wallet) 71 | .store_coins(ton_total) 72 | .store_uint(0x0f8a7ea5, 107 + 32) 73 | .store_uint(query_id, 64) 74 | .store_coins(jettons) 75 | .store_slice(destination) 76 | .store_uint(0, 2 + 1 + 4 + 1) 77 | .end_cell(), 1); 78 | 79 | ;; updating storage 80 | cell my_storage = begin_cell() 81 | .store_uint(0xc548e4c7, 32) 82 | .store_uint(public_key, 256) 83 | .store_uint(seqno + 1, 32) 84 | .end_cell(); 85 | return my_storage; 86 | } 87 | 88 | cell process_external(int my_id, cell my_storage, slice in_msg_body) method_id(89430) { 89 | ;; no accept_message() 90 | return my_storage; 91 | } 92 | -------------------------------------------------------------------------------- /func/plugin-seed-in.fc: -------------------------------------------------------------------------------- 1 | int prefix_of(slice, slice) asm "SDPFX"; 2 | int claimed_seed(slice block_seed, builder my_addr) asm "TWO HASHEXT_SHA256"; 3 | int actual_seed() asm "RANDSEED"; 4 | 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | 7 | () main() { 8 | throw(65535); 9 | } 10 | 11 | ;; extern "wallet-v5" 12 | cell process_external(int my_id, cell my_storage, slice in_msg_body) method_id(89430) { 13 | ;; #_ block_seed:uint256 bounty:MsgAddressInt = InMsgBody; 14 | 15 | throw_if(100, my_storage.begin_parse().prefix_of(in_msg_body)); 16 | 17 | slice block_seed = in_msg_body~load_bits(256); 18 | slice me = my_address(); me~skip_bits(11); 19 | 20 | ;; pending fix of https://github.com/ton-blockchain/ton/issues/978, account seed is calculated so 21 | builder hashed = begin_cell().store_slice(me.preload_bits(32)).store_slice(me.preload_bits(224)); 22 | throw_if(101, claimed_seed(block_seed, hashed) ^ actual_seed()); 23 | 24 | slice dest = in_msg_body~load_msg_addr(); 25 | 26 | accept_message(); 27 | 28 | send_raw_message(begin_cell() 29 | .store_uint(0x18, 6) 30 | .store_slice(dest) 31 | .store_coins(40000000) ;; 0.04 TON per seed delivered 32 | .store_uint(0, 107) 33 | .end_cell(), 2); 34 | 35 | return begin_cell() 36 | .store_slice(block_seed) 37 | .end_cell(); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /func/plugin.fc: -------------------------------------------------------------------------------- 1 | ;; extern "wallet-v5" 2 | int seqno() method_id { return 0; } 3 | 4 | ;; extern "wallet-v5" 5 | ;; @returns ECDSA 256-bit public key 6 | slice get_public_key() method_id { return null(); } 7 | 8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 9 | 10 | tuple digitize_number(int value) 11 | asm "NIL WHILE:<{ OVER }>DO<{ SWAP TEN DIVMOD s1 s2 XCHG TPUSH }> NIP"; 12 | 13 | builder store_number(builder msg, tuple t) 14 | asm "WHILE:<{ DUP TLEN }>DO<{ TPOP 48 ADDCONST ROT 8 STU SWAP }> DROP"; 15 | 16 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 17 | 18 | () main() { 19 | throw(65535); 20 | } 21 | 22 | slice creator_address() method_id(119703) { 23 | return "EQCyoez1VF4HbNNq5Rbqfr3zKuoAjKorhK-YZr7LIIiVrSD7"a; 24 | } 25 | 26 | cell process_internal(int my_id, cell my_storage, slice in_msg_body) method_id(121535) { 27 | send_raw_message(begin_cell() 28 | .store_uint(0x30, 6) 29 | .store_uint(0, 98 + 32) 30 | .store_slice("Internal with hash ") 31 | .store_number(in_msg_body.slice_hash().digitize_number()) 32 | .end_cell(), 0); 33 | return my_storage; 34 | } 35 | 36 | cell process_external(int my_id, cell my_storage, slice in_msg_body) method_id(89430) { 37 | ifnot (my_storage.begin_parse().slice_bits()) { 38 | accept_message(); 39 | send_raw_message(begin_cell() 40 | .store_uint(0x30, 6) 41 | .store_uint(0, 98 + 32) 42 | .store_slice("External: ") 43 | .store_slice(in_msg_body) 44 | .store_slice(" at seqno ") 45 | .store_number(seqno().digitize_number()) 46 | .end_cell(), 0); 47 | return begin_cell().store_uint(0, 1).end_cell(); 48 | } 49 | return my_storage; 50 | } 51 | -------------------------------------------------------------------------------- /func/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | {- 5 | # Tuple manipulation primitives 6 | The names and the types are mostly self-explaining. 7 | See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) 8 | for more info on the polymorphic functions. 9 | 10 | Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) 11 | and vise versa. 12 | -} 13 | 14 | {- 15 | # Lisp-style lists 16 | 17 | Lists can be represented as nested 2-elements tuples. 18 | Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). 19 | For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. 20 | -} 21 | 22 | ;;; Adds an element to the beginning of lisp-style list. 23 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 24 | 25 | ;;; Extracts the head and the tail of lisp-style list. 26 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 27 | 28 | ;;; Extracts the tail and the head of lisp-style list. 29 | forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; 30 | 31 | ;;; Returns the head of lisp-style list. 32 | forall X -> X car(tuple list) asm "CAR"; 33 | 34 | ;;; Returns the tail of lisp-style list. 35 | tuple cdr(tuple list) asm "CDR"; 36 | 37 | ;;; Creates tuple with zero elements. 38 | tuple empty_tuple() asm "NIL"; 39 | 40 | ;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` 41 | ;;; is of length at most 255. Otherwise throws a type check exception. 42 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 43 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 44 | 45 | ;;; Creates a tuple of length one with given argument as element. 46 | forall X -> [X] single(X x) asm "SINGLE"; 47 | 48 | ;;; Unpacks a tuple of length one 49 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 50 | 51 | ;;; Creates a tuple of length two with given arguments as elements. 52 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 53 | 54 | ;;; Unpacks a tuple of length two 55 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 56 | 57 | ;;; Creates a tuple of length three with given arguments as elements. 58 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 59 | 60 | ;;; Unpacks a tuple of length three 61 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 62 | 63 | ;;; Creates a tuple of length four with given arguments as elements. 64 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 65 | 66 | ;;; Unpacks a tuple of length four 67 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 68 | 69 | ;;; Returns the first element of a tuple (with unknown element types). 70 | forall X -> X first(tuple t) asm "FIRST"; 71 | 72 | ;;; Returns the second element of a tuple (with unknown element types). 73 | forall X -> X second(tuple t) asm "SECOND"; 74 | 75 | ;;; Returns the third element of a tuple (with unknown element types). 76 | forall X -> X third(tuple t) asm "THIRD"; 77 | 78 | ;;; Returns the fourth element of a tuple (with unknown element types). 79 | forall X -> X fourth(tuple t) asm "3 INDEX"; 80 | 81 | ;;; Returns the first element of a pair tuple. 82 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 83 | 84 | ;;; Returns the second element of a pair tuple. 85 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 86 | 87 | ;;; Returns the first element of a triple tuple. 88 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 89 | 90 | ;;; Returns the second element of a triple tuple. 91 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 92 | 93 | ;;; Returns the third element of a triple tuple. 94 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 95 | 96 | 97 | ;;; Push null element (casted to given type) 98 | ;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. 99 | ;;; So `null` can actually have any atomic type. 100 | forall X -> X null() asm "PUSHNULL"; 101 | 102 | ;;; Moves a variable [x] to the top of the stack 103 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 104 | 105 | 106 | 107 | ;;; Returns the current Unix time as an Integer 108 | int now() asm "NOW"; 109 | 110 | ;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. 111 | ;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. 112 | slice my_address() asm "MYADDR"; 113 | 114 | ;;; Returns the balance of the smart contract as a tuple consisting of an int 115 | ;;; (balance in nanotoncoins) and a `cell` 116 | ;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") 117 | ;;; at the start of Computation Phase. 118 | ;;; Note that RAW primitives such as [send_raw_message] do not update this field. 119 | [int, cell] get_balance() asm "BALANCE"; 120 | 121 | ;;; Returns the logical time of the current transaction. 122 | int cur_lt() asm "LTIME"; 123 | 124 | ;;; Returns the starting logical time of the current block. 125 | int block_lt() asm "BLOCKLT"; 126 | 127 | ;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. 128 | ;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. 129 | int cell_hash(cell c) asm "HASHCU"; 130 | 131 | ;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. 132 | ;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created 133 | ;;; and its hash computed by [cell_hash]. 134 | int slice_hash(slice s) asm "HASHSU"; 135 | 136 | ;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, 137 | ;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. 138 | int string_hash(slice s) asm "SHA256U"; 139 | 140 | {- 141 | # Signature checks 142 | -} 143 | 144 | ;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) 145 | ;;; using [public_key] (also represented by a 256-bit unsigned integer). 146 | ;;; The signature must contain at least 512 data bits; only the first 512 bits are used. 147 | ;;; The result is `−1` if the signature is valid, `0` otherwise. 148 | ;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. 149 | ;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, 150 | ;;; the second hashing occurring inside `CHKSIGNS`. 151 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 152 | 153 | ;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, 154 | ;;; similarly to [check_signature]. 155 | ;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. 156 | ;;; The verification of Ed25519 signatures is the standard one, 157 | ;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. 158 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 159 | 160 | {--- 161 | # Computation of boc size 162 | The primitives below may be useful for computing storage fees of user-provided data. 163 | -} 164 | 165 | ;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. 166 | ;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` 167 | ;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account 168 | ;;; the identification of equal cells. 169 | ;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, 170 | ;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. 171 | ;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; 172 | ;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and 173 | ;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. 174 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 175 | 176 | ;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. 177 | ;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; 178 | ;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. 179 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 180 | 181 | ;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. 182 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 183 | 184 | ;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. 185 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 186 | 187 | ;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) 188 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 189 | 190 | {-- 191 | # Debug primitives 192 | Only works for local TVM execution with debug level verbosity 193 | -} 194 | ;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. 195 | () dump_stack() impure asm "DUMPSTK"; 196 | 197 | {- 198 | # Persistent storage save and load 199 | -} 200 | 201 | ;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. 202 | cell get_data() asm "c4 PUSH"; 203 | 204 | ;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. 205 | () set_data(cell c) impure asm "c4 POP"; 206 | 207 | {- 208 | # Continuation primitives 209 | -} 210 | ;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. 211 | ;;; The primitive returns the current value of `c3`. 212 | cont get_c3() impure asm "c3 PUSH"; 213 | 214 | ;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. 215 | ;;; Note that after execution of this primitive the current code 216 | ;;; (and the stack of recursive function calls) won't change, 217 | ;;; but any other function call will use a function from the new code. 218 | () set_c3(cont c) impure asm "c3 POP"; 219 | 220 | ;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. 221 | cont bless(slice s) impure asm "BLESS"; 222 | 223 | {--- 224 | # Gas related primitives 225 | -} 226 | 227 | ;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, 228 | ;;; decreasing the value of `gr` by `gc` in the process. 229 | ;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. 230 | ;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. 231 | ;;; 232 | ;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). 233 | () accept_message() impure asm "ACCEPT"; 234 | 235 | ;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. 236 | ;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, 237 | ;;; an (unhandled) out of gas exception is thrown before setting new gas limits. 238 | ;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. 239 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 240 | 241 | ;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) 242 | ;;; so that the current execution is considered “successful” with the saved values even if an exception 243 | ;;; in Computation Phase is thrown later. 244 | () commit() impure asm "COMMIT"; 245 | 246 | ;;; Not implemented 247 | ;;() buy_gas(int gram) impure asm "BUYGAS"; 248 | 249 | ;;; Computes the amount of gas that can be bought for `amount` nanoTONs, 250 | ;;; and sets `gl` accordingly in the same way as [set_gas_limit]. 251 | () buy_gas(int amount) impure asm "BUYGAS"; 252 | 253 | ;;; Computes the minimum of two integers [x] and [y]. 254 | int min(int x, int y) asm "MIN"; 255 | 256 | ;;; Computes the maximum of two integers [x] and [y]. 257 | int max(int x, int y) asm "MAX"; 258 | 259 | ;;; Sorts two integers. 260 | (int, int) minmax(int x, int y) asm "MINMAX"; 261 | 262 | ;;; Computes the absolute value of an integer [x]. 263 | int abs(int x) asm "ABS"; 264 | 265 | {- 266 | # Slice primitives 267 | 268 | It is said that a primitive _loads_ some data, 269 | if it returns the data and the remainder of the slice 270 | (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). 271 | 272 | It is said that a primitive _preloads_ some data, if it returns only the data 273 | (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). 274 | 275 | Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. 276 | -} 277 | 278 | 279 | ;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, 280 | ;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) 281 | ;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. 282 | slice begin_parse(cell c) asm "CTOS"; 283 | 284 | ;;; Checks if [s] is empty. If not, throws an exception. 285 | () end_parse(slice s) impure asm "ENDS"; 286 | 287 | ;;; Loads the first reference from the slice. 288 | (slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; 289 | 290 | ;;; Preloads the first reference from the slice. 291 | cell preload_ref(slice s) asm "PLDREF"; 292 | 293 | {- Functions below are commented because are implemented on compilator level for optimisation -} 294 | 295 | ;;; Loads a signed [len]-bit integer from a slice [s]. 296 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 297 | 298 | ;;; Loads an unsigned [len]-bit integer from a slice [s]. 299 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 300 | 301 | ;;; Preloads a signed [len]-bit integer from a slice [s]. 302 | ;; int preload_int(slice s, int len) asm "PLDIX"; 303 | 304 | ;;; Preloads an unsigned [len]-bit integer from a slice [s]. 305 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 306 | 307 | ;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 308 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 309 | 310 | ;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 311 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 312 | 313 | ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). 314 | (slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; 315 | (slice, int) load_coins(slice s) asm( -> 1 0) "LDGRAMS"; 316 | 317 | ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 318 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 319 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 320 | 321 | ;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 322 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 323 | 324 | ;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 325 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 326 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 327 | 328 | ;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 329 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 330 | 331 | ;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. 332 | ;;; (returns `null` if `nothing` constructor is used). 333 | (slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; 334 | 335 | ;;; Preloads a dictionary `D` from `slice` [s]. 336 | cell preload_dict(slice s) asm "PLDDICT"; 337 | 338 | ;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. 339 | slice skip_dict(slice s) asm "SKIPDICT"; 340 | 341 | ;;; Loads (Maybe ^Cell) from `slice` [s]. 342 | ;;; In other words loads 1 bit and if it is true 343 | ;;; loads first ref and return it with slice remainder 344 | ;;; otherwise returns `null` and slice remainder 345 | (slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; 346 | 347 | ;;; Preloads (Maybe ^Cell) from `slice` [s]. 348 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 349 | 350 | 351 | ;;; Returns the depth of `cell` [c]. 352 | ;;; If [c] has no references, then return `0`; 353 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. 354 | ;;; If [c] is a `null` instead of a cell, returns zero. 355 | int cell_depth(cell c) asm "CDEPTH"; 356 | 357 | 358 | {- 359 | # Slice size primitives 360 | -} 361 | 362 | ;;; Returns the number of references in `slice` [s]. 363 | int slice_refs(slice s) asm "SREFS"; 364 | 365 | ;;; Returns the number of data bits in `slice` [s]. 366 | int slice_bits(slice s) asm "SBITS"; 367 | 368 | ;;; Returns both the number of data bits and the number of references in `slice` [s]. 369 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 370 | 371 | ;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). 372 | int slice_empty?(slice s) asm "SEMPTY"; 373 | 374 | ;;; Checks whether `slice` [s] has no bits of data. 375 | int slice_data_empty?(slice s) asm "SDEMPTY"; 376 | 377 | ;;; Checks whether `slice` [s] has no references. 378 | int slice_refs_empty?(slice s) asm "SREMPTY"; 379 | 380 | ;;; Returns the depth of `slice` [s]. 381 | ;;; If [s] has no references, then returns `0`; 382 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. 383 | int slice_depth(slice s) asm "SDEPTH"; 384 | 385 | {- 386 | # Builder size primitives 387 | -} 388 | 389 | ;;; Returns the number of cell references already stored in `builder` [b] 390 | int builder_refs(builder b) asm "BREFS"; 391 | 392 | ;;; Returns the number of data bits already stored in `builder` [b]. 393 | int builder_bits(builder b) asm "BBITS"; 394 | 395 | ;;; Returns the depth of `builder` [b]. 396 | ;;; If no cell references are stored in [b], then returns 0; 397 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. 398 | int builder_depth(builder b) asm "BDEPTH"; 399 | 400 | {- 401 | # Builder primitives 402 | It is said that a primitive _stores_ a value `x` into a builder `b` 403 | if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. 404 | It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). 405 | 406 | All the primitives below first check whether there is enough space in the `builder`, 407 | and only then check the range of the value being serialized. 408 | -} 409 | 410 | ;;; Creates a new empty `builder`. 411 | builder begin_cell() asm "NEWC"; 412 | 413 | ;;; Converts a `builder` into an ordinary `cell`. 414 | cell end_cell(builder b) asm "ENDC"; 415 | 416 | ;;; Stores a reference to `cell` [c] into `builder` [b]. 417 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 418 | 419 | ;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. 420 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 421 | 422 | ;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. 423 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 424 | 425 | 426 | ;;; Stores `slice` [s] into `builder` [b] 427 | builder store_slice(builder b, slice s) asm "STSLICER"; 428 | 429 | ;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. 430 | ;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, 431 | ;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, 432 | ;;; followed by an `8l`-bit unsigned big-endian representation of [x]. 433 | ;;; If [x] does not belong to the supported range, a range check exception is thrown. 434 | ;;; 435 | ;;; Store amounts of TonCoins to the builder as VarUInteger 16 436 | builder store_grams(builder b, int x) asm "STGRAMS"; 437 | builder store_coins(builder b, int x) asm "STGRAMS"; 438 | 439 | ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. 440 | ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. 441 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 442 | 443 | ;;; Stores (Maybe ^Cell) to builder: 444 | ;;; if cell is null store 1 zero bit 445 | ;;; otherwise store 1 true bit and ref to cell 446 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 447 | 448 | 449 | {- 450 | # Address manipulation primitives 451 | The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: 452 | ```TL-B 453 | addr_none$00 = MsgAddressExt; 454 | addr_extern$01 len:(## 8) external_address:(bits len) 455 | = MsgAddressExt; 456 | anycast_info$_ depth:(#<= 30) { depth >= 1 } 457 | rewrite_pfx:(bits depth) = Anycast; 458 | addr_std$10 anycast:(Maybe Anycast) 459 | workchain_id:int8 address:bits256 = MsgAddressInt; 460 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) 461 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt; 462 | _ _:MsgAddressInt = MsgAddress; 463 | _ _:MsgAddressExt = MsgAddress; 464 | 465 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool 466 | src:MsgAddress dest:MsgAddressInt 467 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams 468 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 469 | ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt 470 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 471 | ``` 472 | A deserialized `MsgAddress` is represented by a tuple `t` as follows: 473 | 474 | - `addr_none` is represented by `t = (0)`, 475 | i.e., a tuple containing exactly one integer equal to zero. 476 | - `addr_extern` is represented by `t = (1, s)`, 477 | where slice `s` contains the field `external_address`. In other words, ` 478 | t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. 479 | - `addr_std` is represented by `t = (2, u, x, s)`, 480 | where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). 481 | Next, integer `x` is the `workchain_id`, and slice `s` contains the address. 482 | - `addr_var` is represented by `t = (3, u, x, s)`, 483 | where `u`, `x`, and `s` have the same meaning as for `addr_std`. 484 | -} 485 | 486 | ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, 487 | ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. 488 | (slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; 489 | 490 | ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. 491 | ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. 492 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 493 | 494 | ;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), 495 | ;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, 496 | ;;; and returns both the workchain and the 256-bit address as integers. 497 | ;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, 498 | ;;; throws a cell deserialization exception. 499 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 500 | 501 | ;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], 502 | ;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). 503 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 504 | 505 | {- 506 | # Dictionary primitives 507 | -} 508 | 509 | 510 | ;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), 511 | ;;; and returns the resulting dictionary. 512 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 513 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 514 | 515 | ;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), 516 | ;;; and returns the resulting dictionary. 517 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 518 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 519 | 520 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 521 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; 522 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; 523 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 524 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 525 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 526 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 527 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 528 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 529 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 530 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 531 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 532 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 533 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 534 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 535 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 536 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 537 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 538 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 539 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 540 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 541 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 542 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 543 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 544 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 545 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 546 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 547 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 548 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 549 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 550 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 551 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 552 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 553 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 554 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 555 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 556 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 557 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 558 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 559 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 560 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 561 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 562 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 563 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 564 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 565 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 566 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 567 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 568 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 569 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 570 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 571 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 572 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 573 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 574 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 575 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 576 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 577 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 578 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 579 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 580 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 581 | 582 | ;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL 583 | cell new_dict() asm "NEWDICT"; 584 | ;;; Checks whether a dictionary is empty. Equivalent to cell_null?. 585 | int dict_empty?(cell c) asm "DICTEMPTY"; 586 | 587 | 588 | {- Prefix dictionary primitives -} 589 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 590 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 591 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 592 | 593 | ;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. 594 | cell config_param(int x) asm "CONFIGOPTPARAM"; 595 | ;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. 596 | int cell_null?(cell c) asm "ISNULL"; 597 | 598 | ;;; Creates an output action which would reserve exactly amount nanotoncoins (if mode = 0), at most amount nanotoncoins (if mode = 2), or all but amount nanotoncoins (if mode = 1 or mode = 3), from the remaining balance of the account. It is roughly equivalent to creating an outbound message carrying amount nanotoncoins (or b − amount nanotoncoins, where b is the remaining balance) to oneself, so that the subsequent output actions would not be able to spend more money than the remainder. Bit +2 in mode means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. Bit +8 in mode means `amount <- -amount` before performing any further actions. Bit +4 in mode means that amount is increased by the original balance of the current account (before the compute phase), including all extra currencies, before performing any other checks and actions. Currently, amount must be a non-negative integer, and mode must be in the range 0..15. 599 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 600 | ;;; Similar to raw_reserve, but also accepts a dictionary extra_amount (represented by a cell or null) with extra currencies. In this way currencies other than TonCoin can be reserved. 601 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 602 | ;;; Sends a raw message contained in msg, which should contain a correctly serialized object Message X, with the only exception that the source address is allowed to have dummy value addr_none (to be automatically replaced with the current smart contract address), and ihr_fee, fwd_fee, created_lt and created_at fields can have arbitrary values (to be rewritten with correct values during the action phase of the current transaction). Integer parameter mode contains the flags. Currently mode = 0 is used for ordinary messages; mode = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message); mode = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message (if bit 0 is not set, the gas fees are deducted from this amount); mode' = mode + 1 means that the sender wants to pay transfer fees separately; mode' = mode + 2 means that any errors arising while processing this message during the action phase should be ignored. Finally, mode' = mode + 32 means that the current account must be destroyed if its resulting balance is zero. This flag is usually employed together with +128. 603 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 604 | ;;; Creates an output action that would change this smart contract code to that given by cell new_code. Notice that this change will take effect only after the successful termination of the current run of the smart contract 605 | () set_code(cell new_code) impure asm "SETCODE"; 606 | 607 | ;;; Generates a new pseudo-random unsigned 256-bit integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x. 608 | int random() impure asm "RANDU256"; 609 | ;;; Generates a new pseudo-random integer z in the range 0..range−1 (or range..−1, if range < 0). More precisely, an unsigned random value x is generated as in random; then z := x * range / 2^256 is computed. 610 | int rand(int range) impure asm "RAND"; 611 | ;;; Returns the current random seed as an unsigned 256-bit Integer. 612 | int get_seed() impure asm "RANDSEED"; 613 | ;;; Sets the random seed to unsigned 256-bit seed. 614 | () set_seed(int) impure asm "SETRAND"; 615 | ;;; Mixes unsigned 256-bit integer x into the random seed r by setting the random seed to sha256 of the concatenation of two 32-byte strings: the first with the big-endian representation of the old seed r, and the second with the big-endian representation of x. 616 | () randomize(int x) impure asm "ADDRAND"; 617 | ;;; Equivalent to randomize(cur_lt());. 618 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 619 | 620 | ;;; Checks whether the data parts of two slices coinside 621 | int equal_slices(slice a, slice b) asm "SDEQ"; 622 | 623 | ;;; Concatenates two builders 624 | builder store_builder(builder to, builder from) asm "STBR"; 625 | -------------------------------------------------------------------------------- /func/wallet.fc: -------------------------------------------------------------------------------- 1 | #pragma version ^0.4.4; 2 | #pragma compute-asm-ltr; 3 | 4 | ;; Wallet inplug-v2 5 | ;; - supports up to 255 actions in one external message 6 | ;; - supports plugins in form of continuations (not independent contracts) 7 | ;; - stores plugins as masterchain libraries via single registry contract 8 | ;; - supports onchain code execution without attaching it as plugin 9 | ;; - supports Ed25519 signatures 10 | 11 | (cell, int) fixed::udict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; 12 | cell end_exotic(builder b) impure asm "ONE ENDXC"; 13 | forall X, Y -> Y unsafe::transmute(X) asm "NOP"; 14 | int equal_slices(slice a, slice b) asm "SDEQ"; 15 | () terminate_if(int) impure asm "IFRETALT"; 16 | () set_c5(cell) impure asm "c5 POPCTR"; 17 | () terminate() impure asm "RETALT"; 18 | 19 | (slice, slice) ~load_ref_parse(slice) asm "LDREFRTOS"; 20 | (slice, ()) ~snake_next(slice) asm "LDREF ENDS CTOS"; 21 | int load_maybe_op(slice) asm "32 PLDUZ"; 22 | 23 | ;; -------------------------- NON-ISOLATED EXECUTION --------------------------- 24 | 25 | () signed_invoke(cont f) impure asm "0 0 CALLXARGS"; 26 | 27 | ;; no need to RUNVM; the plugin is trusted code anyway 28 | cell trust_invoke(cont f, int addr, cell data, slice msg, int method) impure asm(addr data msg method f) 29 | "4 1 CALLXARGS"; 30 | 31 | (cell, int, cell) vm_invoke(cont f, int addr, cell data, slice msg, int method) impure asm(addr data msg method f) 32 | "4 PUSHINT SWAP ONE // addr data msg method in=4 f out=1" 33 | "s5 PUSH // addr data msg method in=4 f out=1 c4=data" 34 | "// (293 sets c3=f, c4=data, returns c4', c5 and one value from stack)" 35 | "293 RUNVM // data' exitcode c4' c5" 36 | "1 1 BLKDROP2 // data' exitcode c5"; 37 | 38 | const int EMPTY_CELL_HASH = 0x96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7; 39 | 40 | ;; ---------------------------------- STORAGE ---------------------------------- 41 | 42 | const int op::invoke_onchain = 0x6908c994; 43 | const int op::install_plugin = 0x79ae2d9f; 44 | const int op::uninstall_plugin = 0x01fabff6; 45 | 46 | const int op::invoke_plugin = 0x44d562b5; 47 | const int op::do_signed = 0x142c7d04; 48 | 49 | ;; limits#_ opcodes_allowed:(HashmapE 32 ()) ton_limit:(VarUInteger 16) = Limits; 50 | ;; plugin_measured$0 code:^Cell data:^Cell access_at:uint33 timeout:uint32 limits:Limits = Plugin; 51 | ;; plugin_trusted$1 code:^Cell data:^Cell = Plugin; 52 | 53 | ;; invoke_onchain$11 code:^Cell next:(Maybe ^Action) = Action; 54 | ;; install_plugin$01 state_init:^Plugin next:(Maybe ^Action) = Action; 55 | ;; uninstall_plugin$00 id:uint256 next:(Maybe ^Action) = Action; 56 | 57 | ;; do_signed_timed#142c7d04 signature:? 58 | ;; request:^[ctr:uint64 deadline:uint32 actions:(Maybe ^Cell) do:(Maybe ^Action)] = ExtInMsgBody; 59 | ;; invoke_plugin#44d562b5 id:uint256 data:Any = ExtInMsgBody; 60 | ;; invoke_plugin#44d562b5 id:uint256 data:Any = IntInMsgBody; 61 | 62 | (int, cell, slice) load_contract() inline { 63 | ;; key::public::ed25519#_ key:bits256 = Key; 64 | ;; _ counter:uint64 plugins_state:(HashmapE 256 ^Plugin) owner_key:Key = Storage; 65 | 66 | slice ds = get_data().begin_parse(); 67 | int counter = ds~load_uint(64); ;; seqno protection, just contains subwallet ID as well 68 | cell plugins = ds~load_dict(); 69 | return (counter, plugins, ds); 70 | } 71 | 72 | () save_contract(slice key, cell plugins, int counter) impure inline { 73 | set_data(begin_cell().store_uint(counter, 64).store_dict(plugins).store_slice(key).end_cell()); 74 | } 75 | ;; -------------------------------- GET-METHODS -------------------------------- 76 | 77 | int seqno() method_id { 78 | return get_data().begin_parse().preload_uint(64); 79 | } 80 | slice get_public_key() method_id { 81 | return get_data().begin_parse().slice_last(256); 82 | } 83 | int is_plugin_installed(int plugin_addr) method_id { 84 | (_, cell plugins, _) = load_contract(); 85 | (_, int ok?) = plugins.udict_get?(256, plugin_addr); 86 | return ok?; 87 | } 88 | 89 | cell check_any_signature(slice in_msg_body, slice key) impure inline { 90 | cell request = in_msg_body~load_ref(); 91 | int request_hash = request.cell_hash(); 92 | throw_unless(310, check_signature(request_hash, in_msg_body, key.preload_uint(256))); 93 | return request; 94 | } 95 | 96 | ;; ------------------------ PLUGIN MANAGEMENT FUNCTIONS ------------------------ 97 | 98 | (cell, ()) state_execute_plugin(cell state, int external?, slice in_msg_body, int plugin_id) impure inline_ref { 99 | slice s = state.begin_parse(); 100 | 101 | if (s~load_uint(1)) { 102 | ;; we have trusted plugin 103 | 104 | (cell code, cell data) = (s~load_ref(), s.preload_ref()); 105 | cell data = s.preload_ref(); 106 | int start_hash = data.cell_hash(); 107 | data = code.begin_parse().bless() 108 | .trust_invoke(plugin_id, data, in_msg_body, external? * 32105 + 121535); 109 | 110 | if (data.cell_hash() == start_hash) { 111 | return (state, ()); 112 | } else { 113 | return (begin_cell().store_dict(code).store_ref(data).end_cell(), ()); 114 | } 115 | } else { 116 | (int access_at, cell code, cell data, int timeout, slice limits) = 117 | (s~load_uint(33), s~load_ref(), s~load_ref(), s~load_uint(32), s); 118 | builder new_state = begin_cell().store_uint(access_at + timeout, 33).store_ref(code).store_uint(timeout, 32).store_slice(limits); 119 | 120 | throw_if(63, external?); ;; we ensure that untrusted plugin is invoked by internal message 121 | throw_if(62, now() < access_at); 122 | (data, int exitcode, cell actions) = code.begin_parse().bless().vm_invoke(plugin_id, data, in_msg_body, 121535); 123 | throw_if(61, exitcode > 1); 124 | cell new_state = new_state.store_ref(data).end_cell(); 125 | 126 | ;; execution successful! validating actions... 127 | 128 | ;; c5| out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); 129 | slice act = actions.begin_parse(); 130 | 131 | ;; prev| out_list_empty$_ = OutList 0; 132 | cell prev = act~load_ref(); throw_unless(60, prev.cell_hash() == EMPTY_CELL_HASH); 133 | 134 | ;; action| action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; 135 | throw_unless(60, act~load_uint(32) == 0x0ec3c86d); 136 | int mode = act~load_uint(8); throw_if(60, mode & 188); ;; only +1, +2 and +64 flags are allowed 137 | cell out_msg = act~load_ref(); 138 | act.end_parse(); 139 | 140 | slice msg = out_msg.begin_parse(); 141 | throw_unless(59, (msg~load_uint(4) & 5) == 4); 142 | msg~load_msg_addr(); 143 | msg~load_msg_addr(); ;; we don't check destination workchain, that would be over board 144 | int value = msg~load_coins(); 145 | msg = msg.skip_dict(); 146 | msg~load_coins(); 147 | msg~load_coins(); 148 | throw_if(59, msg~load_uint(97) & 1); ;; we have to forbid StateInit because it's too complex to parse 149 | 150 | if (msg~load_uint(1)) { msg~snake_next(); } 151 | int op = msg.load_maybe_op(); 152 | 153 | cell opcodes_allowed = limits~load_dict(); 154 | (_, int ok?) = opcodes_allowed.udict_get?(32, op); 155 | throw_unless(58, ok? | (op == 0)); 156 | throw_unless(58, value <= limits~load_coins()); 157 | 158 | ;; since we're invoking untrusted plugin via internal, we have not done any actions yet 159 | set_c5(actions); 160 | return (new_state, ()); 161 | } 162 | } 163 | 164 | (cell, ()) execute_plugin(cell plugins, int external?, slice in_msg_body, int plugin_addr) impure inline { 165 | (cell state, int ok?) = plugins.udict_get_ref?(256, plugin_addr); 166 | throw_unless(128, ok?); 167 | state~state_execute_plugin(external?, in_msg_body, plugin_addr); 168 | return ~udict_set_ref(plugins, 256, plugin_addr, state); 169 | } 170 | 171 | (cell, ()) install_plugin(cell plugins, cell init) inline { 172 | return ~udict_set_ref(plugins, 256, init.cell_hash(), init); 173 | } 174 | (cell, int) uninstall_plugin(cell plugins, int plugin_addr) inline { 175 | return udict_delete?(plugins, 256, plugin_addr); 176 | } 177 | 178 | ;; ------------------------------- ACTION METHOD ------------------------------- 179 | 180 | (cell, cell) ~do(cell plugins, cell action) inline { 181 | slice action = action.begin_parse(); 182 | int op = action~load_uint(2); 183 | 184 | if (op == 0) { 185 | plugins~uninstall_plugin(action.preload_uint(256)); 186 | } elseif (op == 1) { 187 | plugins~install_plugin(action~load_ref()); 188 | } elseif (op == 3) { 189 | action~load_ref_parse().bless().signed_invoke(); 190 | } else { 191 | throw(303); 192 | } 193 | 194 | return (plugins, action.preload_maybe_ref()); 195 | } 196 | 197 | ;; ----------------------- RECV_INTERNAL + RECV_EXTERNAL ----------------------- 198 | 199 | () main(slice in_msg_body) { 200 | terminate_if(in_msg_body.load_maybe_op() != op::invoke_plugin); ;; automatically ignores bounced/empty messages 201 | in_msg_body~skip_bits(32); 202 | int plugin_addr = in_msg_body~load_uint(256); 203 | 204 | (int counter, cell plugins, slice key) = load_contract(); 205 | plugins~execute_plugin(0, in_msg_body, plugin_addr); 206 | save_contract(key, plugins, counter); 207 | } 208 | 209 | () recv_external(slice in_msg_body) { 210 | int op = in_msg_body~load_uint(32); 211 | if (op == op::do_signed) { 212 | (int counter, cell plugins, slice key) = load_contract(); 213 | slice request = in_msg_body.check_any_signature(key).begin_parse(); 214 | throw_unless(312, request~load_uint(64) == counter); 215 | throw_unless(313, request~load_uint(32) >= now()); ;; assert(deadline >= now) 216 | save_contract(key, plugins, counter + 1); commit(); accept_message(); 217 | 218 | cell c5 = request~load_maybe_ref(); 219 | if (~ cell_null?(c5)) { set_c5(c5); } 220 | 221 | cell action = request.preload_maybe_ref(); 222 | while (~ cell_null?(action)) { action = plugins~do(action); } 223 | 224 | save_contract(key, plugins, counter + 1); 225 | } elseif (op == op::invoke_plugin) { 226 | (int counter, cell plugins, slice key) = load_contract(); 227 | int plugin_addr = in_msg_body~load_uint(256); 228 | plugins~execute_plugin(-1, in_msg_body, plugin_addr); ;; plugin is responsible for ACCEPT 229 | save_contract(key, plugins, counter); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@orbs-network/ton-access": "^2.3.3", 4 | "@ton-community/func-js": "^0.6.3-tvmbeta.3", 5 | "@ton/sandbox": "0.16.0-tvmbeta.3", 6 | "@ton/ton": "^13.11.1", 7 | "@types/node": "^20.11.30", 8 | "@types/yargs": "^17.0.32", 9 | "npx": "^10.2.2", 10 | "tvm-disassembler": "^3.0.0", 11 | "yargs": "^17.7.2" 12 | }, 13 | "lib": "es2021" 14 | } 15 | -------------------------------------------------------------------------------- /test-wallet-inplug-v2.ts: -------------------------------------------------------------------------------- 1 | import { beginCell, Cell, SendMode, toNano, storeCommonMessageInfo } from "@ton/core"; 2 | import { keyPairFromSeed, sha256_sync } from "@ton/crypto"; 3 | import { Blockchain } from "@ton/sandbox"; 4 | 5 | import { makeSender, WalletInplugV2 } from "./wallet-inplug-v2"; 6 | import { compilePlugin, disasm } from "./build"; 7 | 8 | 9 | function expect(a: any) { 10 | const obj = { 11 | toBe: function(b: any) { 12 | if (a === b) return; 13 | 14 | // throw new Error("mismatch between " + a + " " + b); 15 | console.error("mismatch between " + a + " " + b); 16 | }, 17 | else: function(f: () => any) { 18 | if (!a) return expect(f()); 19 | return obj; 20 | } 21 | }; 22 | return obj; 23 | } 24 | 25 | (async () => { 26 | const blockchain = await Blockchain.create(); 27 | const deployer = await blockchain.treasury('deployer'); 28 | let key = keyPairFromSeed(sha256_sync('v5r2-treasure')); 29 | 30 | let contract = blockchain.openContract(await WalletInplugV2.create({ workchain: 0, publicKey: key.publicKey })); 31 | expect(contract.init.code.hash().toString('base64')).toBe('pA2xGDpPrpTUWg6Z1pEGck9WIqU6y9A1sbJqa1Js5BA='); 32 | expect(contract.init.code.toBoc({idx: false, crc32: true}).toString('base64')) 33 | .toBe('te6cckECDwEAAoEAART/APSkE/S88sgLAQIBIAIJAgFIAwQBftAg1xCCEETVYrW94wiAINch0//tRNDTP/QEEnBARVMDgwf0D2+h8uCAVBMj2zwCgwf0F1jIyz/0AAHPFsntVAoCAUgFCAIBWAYHACWynftRNDTP/QEMDGDB/QOb6ExgABOw5ztRNCDB9cigABG4yX7UTQ1ws/gCkvLTHyGCEBQsfQS6jroBghBE1WK1uo6t7UTQ0z/0BAPT/38CUwODB/QPb6Hy4IBUEyPbPAKDB/QXAcjLP/QAAc8Wye1UkTDi4w0KDQHwI9DTAAGOKtTXTCD5ACLQ7R4GgX1pqIIB2r+gECNFBtpBIPkAUAO6kVvgbBLI9ADMyeA0A9Mg1NTTH8hTUqAByyBSQMwSyx8hzxYG8n/4I1AEufJ+AdDtHhA1QVCCAdq/AXQBcSXbQSVsEQHCAfJ9AszJIdDUAfkACwH+gvCWopbSJPKFxnvuk8MPijCRV/Dao13FuH5BC3hjCgnPx7ryvNMfAYIQDsPIbbryvNMHAYEAvLDyfNTR0NMDAXWwwATyu/pAMfpAMfoA9AH6ADH6ADHTYAFxsPJ70wABk9TR0N7XEAT0BFJSgCD0Dm+hMQXAABWx8roD+gAwEwwACrvyuu1VAcQx7UTQ0z/0BFEzAdQh+QAD1wv/ECP5EPLhNtDTP1ETuvLhONMfAfgjvvLhOSKkVGQwyMs/9AABzxbJ7VT4D/gA9AQhbrOTAe1VkTHi9AWTIG6ziugwAaTIyz/0AAHPFsntVA4AdNDTASHAAJwxINcL/xIBgwf0WzCOISHAAZwx1FmDByH5AEAz9BefAcADldXtHtoAk/LBL+IB4uIB9AUfcGS8'); 34 | 35 | let balance = await contract.getBalance(); 36 | expect(contract.address.toString()).toBe('EQDoYL6oLDYf6GDMpDmsxoYvuLTP8qQNeiWTtFUkHUiQ8-uP'); 37 | expect(balance).toBe(0n); 38 | 39 | await contract.sendDeploy(deployer.getSender(), '5.5'); 40 | balance = await contract.getBalance(); 41 | expect(balance <= toNano('5.50')).toBe(true); 42 | expect(balance >= toNano('5.49')).toBe(true); 43 | 44 | // sending one message 45 | 46 | await makeSender(contract, key.secretKey).send({ 47 | to: deployer.address, value: toNano('0.25'), sendMode: SendMode.NONE 48 | }); 49 | balance = await contract.getBalance(); 50 | expect(balance <= toNano('5.25')).toBe(true); 51 | expect(balance >= toNano('5.22')).toBe(true); 52 | 53 | // plugin installation 54 | 55 | const pluginCode = Cell.fromBase64((await compilePlugin())!); 56 | const plugin = beginCell().storeUint(1, 1).storeRef(pluginCode).storeRef(Cell.EMPTY).endCell(); 57 | 58 | expect(await contract.getIsPluginInstalled(plugin.hash())).toBe(false); 59 | await contract.sendInstallPlugin({secretKey: key.secretKey, code: pluginCode}); 60 | expect(await contract.getIsPluginInstalled(plugin.hash())).toBe(true); 61 | 62 | // plugin invocation via external 63 | 64 | /* 65 | blockchain.setVerbosityForAddress(contract.address, { 66 | blockchainLogs: true, 67 | vmLogs: 'none', 68 | debugLogs: true, 69 | print: true 70 | }); 71 | */ 72 | const trustedPluginRes = await contract.sendInvokePlugin(plugin.hash(), beginCell().storeStringTail('ABC')); 73 | expect(trustedPluginRes.externals.length).toBe(1); 74 | expect(trustedPluginRes.externals[0].body.beginParse().skip(32).loadStringTail()).toBe('External: ABC at seqno 3002109945798721538'); 75 | expect(await contract.getSeqno()).toBe(3002109945798721538n); 76 | 77 | // plugin invocation via internal 78 | 79 | const internalRes = await deployer.send({ 80 | to: contract.address, 81 | value: toNano('0.4'), 82 | body: beginCell().storeUint(0x44d562b5, 32).storeBuffer(plugin.hash()).storeStringTail('ABC').endCell() 83 | }); 84 | const validHash = beginCell().storeStringTail('ABC').endCell().hash(); 85 | expect(internalRes.externals.length).toBe(1); 86 | expect(internalRes.externals[0].body.beginParse().skip(32).loadStringTail()).toBe('Internal with hash 76439435600465928926908238590329271168192884407773227420621521707826431831530'); 87 | expect(validHash.toString('hex')).toBe(76439435600465928926908238590329271168192884407773227420621521707826431831530n.toString(16)); 88 | 89 | // onchain invoking code 90 | 91 | const codeCell = beginCell().storeBuffer(Buffer.from('FF00F807FE20', 'hex')).endCell(); 92 | expect(await disasm(codeCell)).toBe('SETCP0\nx{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}x{F807FE20}'); 93 | 94 | const invocationRes = await contract.sendExecuteCode({secretKey: key.secretKey, code: codeCell}); 95 | const d = invocationRes.transactions[0].description; 96 | expect(invocationRes.transactions[0].debugLogs).toBe('#DEBUG#: s0 = 3195'); 97 | expect(d.type == 'generic' && d.computePhase.type == 'vm' && d.computePhase.gasUsed == 4089n) 98 | .else(() => {console.log(d); return false;}).toBe(true); 99 | })(); 100 | -------------------------------------------------------------------------------- /wallet-inplug-v2.ts: -------------------------------------------------------------------------------- 1 | import { SandboxContract } from "@ton/sandbox"; 2 | import { Address, beginCell, Builder, Cell, Contract, contractAddress, ContractProvider, internal, MessageRelaxed, Sender, SenderArguments, SendMode, storeMessageRelaxed, toNano } from "@ton/core"; 3 | import { sign } from "@ton/crypto"; 4 | import { walletBoc } from "./build"; 5 | 6 | export type Transfer = {action: 'transfer', message: MessageRelaxed, sendMode: SendMode}; 7 | export type UninstallPlugin = {action: 'uninstall', hash: Buffer}; 8 | export type InstallPlugin = {action: 'install', init: Cell}; 9 | export type InvokeCode = {action: 'invoke', code: Cell}; 10 | export type Action = InstallPlugin | InvokeCode | Transfer | UninstallPlugin; 11 | 12 | export class WalletInplugV2 implements Contract { 13 | readonly workchain: number; 14 | readonly publicKey: Buffer; 15 | readonly address: Address; 16 | readonly walletId: number; 17 | readonly init: { data: Cell, code: Cell }; 18 | 19 | static async create(args: {workchain: number, publicKey: Buffer, walletId?: number}) { 20 | let {workchain, publicKey, walletId} = args; 21 | let code = await walletBoc; 22 | return new WalletInplugV2(workchain, publicKey, Cell.fromBase64(code!), walletId); 23 | } 24 | 25 | private constructor(workchain: number, publicKey: Buffer, code: Cell, walletId?: number) { 26 | this.workchain = workchain; 27 | this.publicKey = publicKey; 28 | if (walletId !== undefined) { 29 | this.walletId = walletId; 30 | } else { 31 | this.walletId = 698983191 + workchain; 32 | } 33 | 34 | let data = beginCell() 35 | .storeUint(this.walletId, 32) // subwallet_id -\ uid 36 | .storeUint(0, 32) // seqno _/ 37 | .storeUint(0, 1) // plugins' state 38 | .storeBuffer(this.publicKey) 39 | .endCell(); 40 | this.init = { code, data }; 41 | this.address = contractAddress(workchain, { code, data }); 42 | } 43 | 44 | async getBalance(provider: ContractProvider) { 45 | let state = await provider.getState(); 46 | return state.balance; 47 | } 48 | 49 | async _nowrapGetSeqno(provider: ContractProvider): Promise { 50 | let state = await provider.getState(); 51 | if (state.state.type === 'active') { 52 | let res = await provider.get('seqno', []); 53 | return res.stack.readBigNumber(); 54 | } else { 55 | return BigInt(this.walletId) * 4294967296n + 0n; 56 | } 57 | } 58 | 59 | async getSeqno(provider: ContractProvider): Promise { 60 | return this._nowrapGetSeqno(provider); 61 | } 62 | 63 | async getIsPluginInstalled(provider: ContractProvider, id: Buffer): Promise { 64 | let state = await provider.getState(); 65 | if (state.state.type === 'active') { 66 | let cell_id = beginCell().storeBuffer(id, 32).endCell(); 67 | let num_id = cell_id.beginParse().loadUintBig(256); 68 | 69 | let res = await provider.get('is_plugin_installed', [{type: 'int', value: num_id}]); 70 | return res.stack.readBoolean(); 71 | } else { 72 | return false; 73 | } 74 | } 75 | 76 | async sendExternal(provider: ContractProvider, message: Cell) { 77 | await provider.external(message); 78 | } 79 | 80 | async sendTransfer(provider: ContractProvider, args: { 81 | seqno: bigint, 82 | secretKey: Buffer, 83 | messages: MessageRelaxed[] 84 | sendMode?: SendMode, 85 | timeout?: number, 86 | }) { 87 | let transfer = this.createTransfer(args); 88 | await this.sendExternal(provider, transfer); 89 | } 90 | 91 | _storeAction(nextAction: Cell | null, add: Action): Cell | null { 92 | if (add.action == 'install') { 93 | return beginCell() 94 | .storeUint(1, 2) 95 | .storeRef(add.init) 96 | .storeMaybeRef(nextAction) 97 | .endCell(); 98 | } else if (add.action == 'invoke') { 99 | return beginCell() 100 | .storeUint(3, 2) 101 | .storeRef(add.code) 102 | .storeMaybeRef(nextAction) 103 | .endCell(); 104 | } else if (add.action == 'uninstall') { 105 | return beginCell() 106 | .storeUint(0, 2) 107 | .storeBuffer(add.hash) 108 | .storeMaybeRef(nextAction) 109 | .endCell(); 110 | } else { 111 | throw new Error("trying to store unsupported action"); 112 | } 113 | } 114 | 115 | _storeTransfer(nextC5: Cell, add: Action): Cell { 116 | if (add.action == 'transfer') { 117 | return beginCell() 118 | .storeRef(nextC5) 119 | .storeUint(0x0ec3c86d, 32) 120 | .storeUint(add.sendMode | SendMode.IGNORE_ERRORS, 8) 121 | .storeRef(beginCell().store(storeMessageRelaxed(add.message))) 122 | .endCell(); 123 | } else { 124 | throw new Error("trying to store unsupported action"); 125 | } 126 | } 127 | 128 | signMultiAction(args: { 129 | seqno: bigint, 130 | secretKey: Buffer, 131 | actions: Action[], 132 | }): Cell { 133 | let {seqno, secretKey, actions} = args; 134 | 135 | if (actions.length > 255) { 136 | throw Error('Maximum number of messages in a single transfer is 255'); 137 | } 138 | actions.reverse(); 139 | 140 | let lastActionRepr: Cell | null = null; 141 | let lastC5: Cell = Cell.EMPTY; 142 | for (let a of actions) { 143 | if (a.action == 'transfer') 144 | lastC5 = this._storeTransfer(lastC5, a); 145 | else 146 | lastActionRepr = this._storeAction(lastActionRepr, a); 147 | } 148 | const deadline = Math.floor(Date.now() / 1000) + 60; 149 | const request = beginCell().storeUint(seqno, 64).storeUint(deadline, 32).storeMaybeRef(lastC5).storeMaybeRef(lastActionRepr).endCell(); 150 | const signature: Buffer = sign(request.hash(), secretKey); 151 | const body = beginCell().storeUint(0x142c7d04, 32).storeBuffer(signature).storeRef(request).endCell(); 152 | return body; 153 | } 154 | 155 | createTransfer(args: { 156 | seqno: bigint, 157 | secretKey: Buffer, 158 | messages: MessageRelaxed[], 159 | sendMode?: SendMode | null, 160 | }): Cell { 161 | let {seqno, secretKey, messages, sendMode} = args; 162 | let fixedSendMode : SendMode = (sendMode ?? SendMode.PAY_GAS_SEPARATELY) | SendMode.IGNORE_ERRORS; 163 | 164 | const actions: Action[] = messages.map( 165 | m => {return {action: 'transfer', message: m, sendMode: fixedSendMode};} 166 | ); 167 | return this.signMultiAction({seqno, actions, secretKey}); 168 | } 169 | 170 | async sendExecuteCode(provider: ContractProvider, args: { 171 | seqno?: bigint, 172 | secretKey: Buffer, 173 | code: Cell 174 | }): Promise { 175 | let transfer = this.signMultiAction({ 176 | seqno: args.seqno ?? await this._nowrapGetSeqno(provider), 177 | secretKey: args.secretKey, 178 | actions: [{action: 'invoke', code: args.code}] 179 | }) 180 | await this.sendExternal(provider, transfer); 181 | } 182 | 183 | async sendInstallPlugin(provider: ContractProvider, args: { 184 | seqno?: bigint, 185 | secretKey: Buffer, 186 | code: Cell, 187 | data?: Cell 188 | }): Promise { 189 | let state = beginCell().storeUint(1, 1).storeRef(args.code).storeRef(args.data ?? Cell.EMPTY).endCell(); 190 | let transfer = this.signMultiAction({ 191 | seqno: args.seqno ?? await this._nowrapGetSeqno(provider), 192 | secretKey: args.secretKey, 193 | actions: [{action: 'install', init: state}] 194 | }) 195 | await this.sendExternal(provider, transfer); 196 | return state.hash(); 197 | } 198 | 199 | async sendUninstallPlugin(provider: ContractProvider, args: { 200 | seqno?: bigint, 201 | secretKey: Buffer, 202 | hash?: Buffer, 203 | code?: Cell, 204 | data?: Cell 205 | }) { 206 | let hash: Buffer = args.hash ?? 207 | beginCell().storeUint(1, 1).storeRef(args.code!).storeRef(args.data! ?? Cell.EMPTY).endCell().hash(); 208 | let transfer = this.signMultiAction({ 209 | seqno: args.seqno ?? await this._nowrapGetSeqno(provider), 210 | secretKey: args.secretKey, 211 | actions: [{action: 'uninstall', hash}] 212 | }) 213 | await this.sendExternal(provider, transfer); 214 | } 215 | 216 | async sendDeploy(provider: ContractProvider, via: Sender, value?: string) { 217 | await provider.internal(via, { 218 | value: toNano(value ?? '0.01'), 219 | bounce: false, 220 | sendMode: SendMode.PAY_GAS_SEPARATELY 221 | }); 222 | } 223 | 224 | async sendInvokePlugin(provider: ContractProvider, id: Buffer, data: Builder) { 225 | const body = beginCell().storeUint(0x44d562b5, 32).storeBuffer(id, 32).storeBuilder(data).endCell(); 226 | await this.sendExternal(provider, body); 227 | } 228 | } 229 | 230 | export function makeSender(contract: SandboxContract, secretKey: Buffer) : Sender { 231 | return { 232 | send: async (args: SenderArguments) => { 233 | let seqno = await contract.getSeqno(); 234 | let transfer = contract.createTransfer({ 235 | seqno, 236 | secretKey, 237 | sendMode: args.sendMode, 238 | messages: [internal(args)] 239 | }); 240 | await contract.sendExternal(transfer); 241 | } 242 | }; 243 | } 244 | --------------------------------------------------------------------------------