├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── audits └── ton-blockchain_highload-wallet-contract-v3_2025-04-24.pdf ├── build ├── .gitkeep └── HighloadWalletV3.compiled.json ├── compile.fif ├── contracts ├── highload-wallet-v3.func ├── imports │ └── stdlib.fc └── scheme.tlb ├── jest.config.ts ├── package-lock.json ├── package.json ├── tests ├── HighloadWalletV3.spec.ts └── imports │ └── const.ts ├── tsconfig.json ├── utils.ts └── wrappers ├── HighloadQueryId.ts ├── HighloadWalletV3.compile.ts ├── HighloadWalletV3.ts └── MsgGenerator.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "bracketSpacing": true, 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TON - The Open Network 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 | # highload-wallet-contract-v3 2 | 3 | ⚠️ `timeout` must be greater then 0. We recommend using a timeout from 1 hour to 24 hours. 4 | 5 | ⚠️ This highload-wallet has a limit of 8380415 messages per timeout. If you fill the dictionary completely during the timeout, you will have to wait for the timeout before the dictionary is freed. 6 | 7 | ⚠️ Use an `subwallet_id` different from the `subwallet_id`'s of other contracts (regular wallets or vesting wallets). We recommend using `0x10ad` as `subwallet_id`. 8 | 9 | `query_id` is a composite ID consisting of a shift ([0 .. 8191]) and a bitnumber ([0 .. 1022]). Use `HighloadQueryId.ts` wrapper. 10 | 11 | `npm install` 12 | 13 | Build: 14 | 15 | `npm run build` 16 | 17 | Test: 18 | 19 | `npm run test` 20 | 21 | Useful examples can be found below: 22 | * [Withdrawal](https://github.com/toncenter/examples/blob/main/withdrawals-highload.js) 23 | * [Jetton withdrawal](https://github.com/toncenter/examples/blob/main/withdrawals-jettons.js) 24 | * [Batch withdrawal](https://github.com/toncenter/examples/blob/main/withdrawals-highload-batch.js) 25 | * [Jetton batch withdrawal](https://github.com/toncenter/examples/blob/main/withdrawals-jettons-highload-batch.js) 26 | 27 | Author: [Andrew Gutarev](https://github.com/pyAndr3w) 28 | 29 | ## Security 30 | 31 | The highload-wallet-contract-v3 smart contract has been audited by: 32 | - TonTech: [Audit Report](./audits/ton-blockchain_highload-wallet-contract-v3_2025-04-24.pdf) 33 | -------------------------------------------------------------------------------- /audits/ton-blockchain_highload-wallet-contract-v3_2025-04-24.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-blockchain/highload-wallet-contract-v3/f5d9b592bd5ab5c9ae6ca627819929e140f6bd61/audits/ton-blockchain_highload-wallet-contract-v3_2025-04-24.pdf -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-blockchain/highload-wallet-contract-v3/f5d9b592bd5ab5c9ae6ca627819929e140f6bd61/build/.gitkeep -------------------------------------------------------------------------------- /build/HighloadWalletV3.compiled.json: -------------------------------------------------------------------------------- 1 | {"hash":"11acad7955844090f283bf238bc1449871f783e7cc0979408d3f4859483e8525","hashBase64":"EayteVWEQJDyg78ji8FEmHH3g+fMCXlAjT9IWUg+hSU=","hex":"b5ee9c7241021001000228000114ff00f4a413f4bcf2c80b01020120020d02014803040078d020d74bc00101c060b0915be101d0d3030171b0915be0fa4030f828c705b39130e0d31f018210ae42e5a4ba9d8040d721d74cf82a01ed55fb04e030020120050a02027306070011adce76a2686b85ffc00201200809001aabb6ed44d0810122d721d70b3f0018aa3bed44d08307d721d70b1f0201200b0c001bb9a6eed44d0810162d721d70b15800e5b8bf2eda2edfb21ab09028409b0ed44d0810120d721f404f404d33fd315d1058e1bf82325a15210b99f326df82305aa0015a112b992306dde923033e2923033e25230800df40f6fa19ed021d721d70a00955f037fdb31e09130e259800df40f6fa19cd001d721d70a00937fdb31e0915be270801f6f2d48308d718d121f900ed44d0d3ffd31ff404f404d33fd315d1f82321a15220b98e12336df82324aa00a112b9926d32de58f82301de541675f910f2a106d0d31fd4d307d30cd309d33fd315d15168baf2a2515abaf2a6f8232aa15250bcf2a304f823bbf2a35304800df40f6fa199d024d721d70a00f2649130e20e01fe5309800df40f6fa18e13d05004d718d20001f264c858cf16cf8301cf168e1030c824cf40cf8384095005a1a514cf40e2f800c94039800df41704c8cbff13cb1ff40012f40012cb3f12cb15c9ed54f80f21d0d30001f265d3020171b0925f03e0fa4001d70b01c000f2a5fa4031fa0031f401fa0031fa00318060d721d300010f0020f265d2000193d431d19130e272b1fb00b585bf03"} -------------------------------------------------------------------------------- /compile.fif: -------------------------------------------------------------------------------- 1 | "contracts/highload-wallet-v3.fif" include 2 | 3 | 4 | ."⏳ Compiling..." cr cr 5 | 6 | 7 | // convert code cell to hex and base64 8 | 2 boc+>B =: code-bytes 9 | code-bytes B>x =: code-hex 10 | code-bytes B>base64 =: code-base64 11 | 12 | 13 | ."✅ Compiled successfully! Cell BOC hex result:" cr cr 14 | 15 | code-hex type cr cr 16 | 17 | ."⏳ Creating json output..." cr cr 18 | 19 | 20 | // create json template 21 | $<<""" 22 | { 23 | "hex": "", 24 | "base64": "" 25 | } 26 | """ 27 | 12 $| 16 $| 28 | =: json-template-part3 29 | =: json-template-part2 30 | =: json-template-part1 31 | 32 | 33 | // create final json 34 | json-template-part1 35 | code-hex 36 | json-template-part2 37 | code-base64 38 | json-template-part3 39 | $+ $+ $+ $+ =: code-json 40 | 41 | 42 | // write json to file 43 | code-json $>B "build/HighloadWalletV3.compiled.json" B>file 44 | 45 | ."✅ Wrote compilation artifact to build/HighloadWalletV3.compiled.json" cr 46 | -------------------------------------------------------------------------------- /contracts/highload-wallet-v3.func: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | 3 | ;;; Store binary true b{1} into `builder` [b] 4 | builder store_true(builder b) asm "STONE"; 5 | ;;; Stores [x] binary zeroes into `builder` [b]. 6 | builder store_zeroes(builder b, int x) asm "STZEROES"; 7 | ;;; Store `cell` [actions] to register c5 (out actions) 8 | () set_actions(cell actions) impure asm "c5 POP"; 9 | 10 | const int op::internal_transfer = 0xae42e5a4; 11 | 12 | const int error::invalid_signature = 33; 13 | const int error::invalid_subwallet_id = 34; 14 | const int error::invalid_created_at = 35; 15 | const int error::already_executed = 36; 16 | const int error::invalid_message_to_send = 37; 17 | const int error::invalid_timeout = 38; 18 | 19 | const int KEY_SIZE = 13; 20 | const int SIGNATURE_SIZE = 512; 21 | const int PUBLIC_KEY_SIZE = 256; 22 | const int SUBWALLET_ID_SIZE = 32; 23 | const int TIMESTAMP_SIZE = 64; 24 | const int TIMEOUT_SIZE = 22; ;; 2^22 / 60 / 60 / 24 - up to ~48 days 25 | 26 | const int CELL_BITS_SIZE = 1023; 27 | const int BIT_NUMBER_SIZE = 10; ;; 2^10 = 1024 28 | 29 | () recv_internal(cell in_msg_full, slice in_msg_body) impure { 30 | (int body_bits, int body_refs) = in_msg_body.slice_bits_refs(); 31 | ifnot ((body_refs == 1) & (body_bits == MSG_OP_SIZE + MSG_QUERY_ID_SIZE)) { 32 | return (); ;; just accept TONs 33 | } 34 | 35 | slice in_msg_full_slice = in_msg_full.begin_parse(); 36 | int msg_flags = in_msg_full_slice~load_msg_flags(); 37 | if (msg_flags & 1) { ;; is bounced 38 | return (); 39 | } 40 | 41 | slice sender_address = in_msg_full_slice~load_msg_addr(); 42 | 43 | ;; not from myself 44 | if (~ sender_address.equal_slices_bits(my_address())) { 45 | return (); ;; just accept TONs 46 | } 47 | 48 | int op = in_msg_body~load_op(); 49 | 50 | if (op == op::internal_transfer) { 51 | in_msg_body~skip_query_id(); 52 | cell actions = in_msg_body.preload_ref(); 53 | cell old_code = my_code(); 54 | set_actions(actions); 55 | set_code(old_code); ;; prevent to change smart contract code 56 | return (); 57 | } 58 | } 59 | 60 | () recv_external(slice msg_body) impure { 61 | cell msg_inner = msg_body~load_ref(); 62 | slice signature = msg_body~load_bits(SIGNATURE_SIZE); 63 | msg_body.end_parse(); 64 | int msg_inner_hash = msg_inner.cell_hash(); 65 | 66 | slice data_slice = get_data().begin_parse(); 67 | int public_key = data_slice~load_uint(PUBLIC_KEY_SIZE); 68 | int subwallet_id = data_slice~load_uint(SUBWALLET_ID_SIZE); 69 | cell old_queries = data_slice~load_dict(); 70 | cell queries = data_slice~load_dict(); 71 | int last_clean_time = data_slice~load_uint(TIMESTAMP_SIZE); 72 | int timeout = data_slice~load_uint(TIMEOUT_SIZE); 73 | data_slice.end_parse(); 74 | 75 | if (last_clean_time < (now() - timeout)) { 76 | (old_queries, queries) = (queries, null()); 77 | if (last_clean_time < (now() - (timeout * 2))) { 78 | old_queries = null(); 79 | } 80 | last_clean_time = now(); 81 | } 82 | 83 | throw_unless(error::invalid_signature, check_signature(msg_inner_hash, signature, public_key)); 84 | 85 | slice msg_inner_slice = msg_inner.begin_parse(); 86 | int _subwallet_id = msg_inner_slice~load_uint(SUBWALLET_ID_SIZE); 87 | cell message_to_send = msg_inner_slice~load_ref(); 88 | int send_mode = msg_inner_slice~load_uint(8); 89 | int shift = msg_inner_slice~load_uint(KEY_SIZE); 90 | int bit_number = msg_inner_slice~load_uint(BIT_NUMBER_SIZE); 91 | int created_at = msg_inner_slice~load_uint(TIMESTAMP_SIZE); 92 | int _timeout = msg_inner_slice~load_uint(TIMEOUT_SIZE); 93 | msg_inner_slice.end_parse(); 94 | 95 | throw_unless(error::invalid_subwallet_id, _subwallet_id == subwallet_id); 96 | throw_unless(error::invalid_timeout, _timeout == timeout); 97 | 98 | throw_unless(error::invalid_created_at, created_at > now() - timeout); 99 | throw_unless(error::invalid_created_at, created_at <= now()); 100 | 101 | (cell value, int found) = old_queries.udict_get_ref?(KEY_SIZE, shift); 102 | if (found) { 103 | slice value_slice = value.begin_parse(); 104 | value_slice~skip_bits(bit_number); 105 | throw_if(error::already_executed, value_slice.preload_int(1)); 106 | } 107 | 108 | (cell value, int found) = queries.udict_get_ref?(KEY_SIZE, shift); 109 | builder new_value = null(); 110 | if (found) { 111 | slice value_slice = value.begin_parse(); 112 | (slice tail, slice head) = value_slice.load_bits(bit_number); 113 | throw_if(error::already_executed, tail~load_int(1)); 114 | new_value = begin_cell().store_slice(head).store_true().store_slice(tail); 115 | } else { 116 | new_value = begin_cell().store_zeroes(bit_number).store_true().store_zeroes(CELL_BITS_SIZE - bit_number - 1); 117 | } 118 | 119 | accept_message(); 120 | 121 | queries~udict_set_ref(KEY_SIZE, shift, new_value.end_cell()); 122 | 123 | set_data(begin_cell() 124 | .store_uint(public_key, PUBLIC_KEY_SIZE) 125 | .store_uint(subwallet_id, SUBWALLET_ID_SIZE) 126 | .store_dict(old_queries) 127 | .store_dict(queries) 128 | .store_uint(last_clean_time, TIMESTAMP_SIZE) 129 | .store_uint(timeout, TIMEOUT_SIZE) 130 | .end_cell()); 131 | 132 | 133 | commit(); 134 | 135 | ;; after commit, check the message to prevent an error in the action phase 136 | 137 | slice message_slice = message_to_send.begin_parse(); 138 | {- 139 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L123C1-L124C33 140 | currencies$_ grams:Grams other:ExtraCurrencyCollection = CurrencyCollection; 141 | extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32)) = ExtraCurrencyCollection; 142 | 143 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L135 144 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool 145 | src:MsgAddress dest:MsgAddressInt 146 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams 147 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 148 | 149 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L155 150 | message$_ {X:Type} info:CommonMsgInfoRelaxed 151 | init:(Maybe (Either StateInit ^StateInit)) 152 | body:(Either X ^X) = MessageRelaxed X; 153 | -} 154 | 155 | throw_if(error::invalid_message_to_send, message_slice~load_uint(1)); ;; int_msg_info$0 156 | int msg_flags = message_slice~load_uint(3); ;; ihr_disabled:Bool bounce:Bool bounced:Bool 157 | if (msg_flags & 1) { ;; is bounced 158 | return (); 159 | } 160 | slice message_source_adrress = message_slice~load_msg_addr(); ;; src 161 | throw_unless(error::invalid_message_to_send, is_address_none(message_source_adrress)); 162 | message_slice~load_msg_addr(); ;; dest 163 | message_slice~load_coins(); ;; value.coins 164 | message_slice = message_slice.skip_dict(); ;; value.other extra-currencies 165 | message_slice~load_coins(); ;; ihr_fee 166 | message_slice~load_coins(); ;; fwd_fee 167 | message_slice~skip_bits(64 + 32); ;; created_lt:uint64 created_at:uint32 168 | int maybe_state_init = message_slice~load_uint(1); 169 | throw_if(error::invalid_message_to_send, maybe_state_init); ;; throw if state-init included (state-init not supported) 170 | int either_body = message_slice~load_int(1); 171 | if (either_body) { 172 | message_slice~load_ref(); 173 | message_slice.end_parse(); 174 | } 175 | 176 | ;; send message with IGNORE_ERRORS flag to ignore errors in the action phase 177 | 178 | send_raw_message(message_to_send, send_mode | SEND_MODE_IGNORE_ERRORS); 179 | } 180 | 181 | 182 | int get_public_key() method_id { 183 | return get_data().begin_parse().preload_uint(PUBLIC_KEY_SIZE); 184 | } 185 | 186 | int get_subwallet_id() method_id { 187 | slice data_slice = get_data().begin_parse(); 188 | data_slice~skip_bits(PUBLIC_KEY_SIZE); ;; skip public_key 189 | return data_slice.preload_uint(SUBWALLET_ID_SIZE); 190 | } 191 | 192 | int get_last_clean_time() method_id { 193 | slice data_slice = get_data().begin_parse(); 194 | data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE + 1 + 1); ;; skip: public_key, subwallet_id, old_queried, queries 195 | return data_slice.preload_uint(TIMESTAMP_SIZE); 196 | } 197 | 198 | int get_timeout() method_id { 199 | slice data_slice = get_data().begin_parse(); 200 | data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE + 1 + 1 + TIMESTAMP_SIZE); ;; skip: public_key, subwallet_id, old_queried, queries, last_clean_time 201 | return data_slice.preload_uint(TIMEOUT_SIZE); 202 | } 203 | 204 | int processed?(int query_id, int need_clean) method_id { 205 | int shift = query_id >> BIT_NUMBER_SIZE; 206 | int bit_number = query_id & CELL_BITS_SIZE; 207 | 208 | slice data_slice = get_data().begin_parse(); 209 | data_slice~skip_bits(PUBLIC_KEY_SIZE + SUBWALLET_ID_SIZE); ;; skip: public_key, subwallet_id 210 | cell old_queries = data_slice~load_dict(); 211 | cell queries = data_slice~load_dict(); 212 | int last_clean_time = data_slice~load_uint(TIMESTAMP_SIZE); 213 | int timeout = data_slice~load_uint(TIMEOUT_SIZE); 214 | data_slice.end_parse(); 215 | 216 | if (need_clean) { 217 | if (last_clean_time < (now() - timeout)) { 218 | (old_queries, queries) = (queries, null()); 219 | if (last_clean_time < (now() - (timeout * 2))) { 220 | old_queries = null(); 221 | } 222 | last_clean_time = now(); 223 | } 224 | } 225 | 226 | (cell value, int found) = old_queries.udict_get_ref?(KEY_SIZE, shift); 227 | if (found) { 228 | slice value_slice = value.begin_parse(); 229 | value_slice~skip_bits(bit_number); 230 | if (value_slice.preload_int(1)) { 231 | return TRUE; 232 | } 233 | } 234 | 235 | (cell value, int found) = queries.udict_get_ref?(KEY_SIZE, shift); 236 | if (found) { 237 | slice value_slice = value.begin_parse(); 238 | value_slice~skip_bits(bit_number); 239 | if (value_slice.preload_int(1)) { 240 | return TRUE; 241 | } 242 | } 243 | 244 | return FALSE; 245 | } -------------------------------------------------------------------------------- /contracts/imports/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | {- 5 | This file is part of TON FunC Standard Library. 6 | 7 | FunC Standard Library is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU Lesser General Public License as published by 9 | the Free Software Foundation, either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | FunC Standard Library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU Lesser General Public License for more details. 16 | 17 | -} 18 | 19 | {- 20 | # Tuple manipulation primitives 21 | The names and the types are mostly self-explaining. 22 | See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) 23 | for more info on the polymorphic functions. 24 | 25 | Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) 26 | and vise versa. 27 | -} 28 | 29 | {- 30 | # Lisp-style lists 31 | 32 | Lists can be represented as nested 2-elements tuples. 33 | Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). 34 | For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. 35 | -} 36 | 37 | ;;; Adds an element to the beginning of lisp-style list. 38 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 39 | 40 | ;;; Extracts the head and the tail of lisp-style list. 41 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 42 | 43 | ;;; Extracts the tail and the head of lisp-style list. 44 | forall X -> (tuple, X) list_next(tuple list) asm(-> 1 0) "UNCONS"; 45 | 46 | ;;; Returns the head of lisp-style list. 47 | forall X -> X car(tuple list) asm "CAR"; 48 | 49 | ;;; Returns the tail of lisp-style list. 50 | tuple cdr(tuple list) asm "CDR"; 51 | 52 | ;;; Creates tuple with zero elements. 53 | tuple empty_tuple() asm "NIL"; 54 | 55 | ;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` 56 | ;;; is of length at most 255. Otherwise throws a type check exception. 57 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 58 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 59 | 60 | ;;; Creates a tuple of length one with given argument as element. 61 | forall X -> [X] single(X x) asm "SINGLE"; 62 | 63 | ;;; Unpacks a tuple of length one 64 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 65 | 66 | ;;; Creates a tuple of length two with given arguments as elements. 67 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 68 | 69 | ;;; Unpacks a tuple of length two 70 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 71 | 72 | ;;; Creates a tuple of length three with given arguments as elements. 73 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 74 | 75 | ;;; Unpacks a tuple of length three 76 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 77 | 78 | ;;; Creates a tuple of length four with given arguments as elements. 79 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 80 | 81 | ;;; Unpacks a tuple of length four 82 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 83 | 84 | ;;; Returns the first element of a tuple (with unknown element types). 85 | forall X -> X first(tuple t) asm "FIRST"; 86 | 87 | ;;; Returns the second element of a tuple (with unknown element types). 88 | forall X -> X second(tuple t) asm "SECOND"; 89 | 90 | ;;; Returns the third element of a tuple (with unknown element types). 91 | forall X -> X third(tuple t) asm "THIRD"; 92 | 93 | ;;; Returns the fourth element of a tuple (with unknown element types). 94 | forall X -> X fourth(tuple t) asm "3 INDEX"; 95 | 96 | ;;; Returns the first element of a pair tuple. 97 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 98 | 99 | ;;; Returns the second element of a pair tuple. 100 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 101 | 102 | ;;; Returns the first element of a triple tuple. 103 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 104 | 105 | ;;; Returns the second element of a triple tuple. 106 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 107 | 108 | ;;; Returns the third element of a triple tuple. 109 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 110 | 111 | 112 | ;;; Push null element (casted to given type) 113 | ;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. 114 | ;;; So `null` can actually have any atomic type. 115 | forall X -> X null() asm "PUSHNULL"; 116 | 117 | ;;; Moves a variable [x] to the top of the stack 118 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 119 | 120 | 121 | 122 | ;;; Returns the current Unix time as an Integer 123 | int now() asm "NOW"; 124 | 125 | ;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. 126 | ;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. 127 | slice my_address() asm "MYADDR"; 128 | 129 | ;;; Returns the balance of the smart contract as a tuple consisting of an int 130 | ;;; (balance in nanotoncoins) and a `cell` 131 | ;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") 132 | ;;; at the start of Computation Phase. 133 | ;;; Note that RAW primitives such as [send_raw_message] do not update this field. 134 | [int, cell] get_balance() asm "BALANCE"; 135 | 136 | ;;; Returns the logical time of the current transaction. 137 | int cur_lt() asm "LTIME"; 138 | 139 | ;;; Returns the starting logical time of the current block. 140 | int block_lt() asm "BLOCKLT"; 141 | 142 | ;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. 143 | ;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. 144 | int cell_hash(cell c) asm "HASHCU"; 145 | 146 | ;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. 147 | ;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created 148 | ;;; and its hash computed by [cell_hash]. 149 | int slice_hash(slice s) asm "HASHSU"; 150 | 151 | ;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, 152 | ;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. 153 | int string_hash(slice s) asm "SHA256U"; 154 | 155 | {- 156 | # Signature checks 157 | -} 158 | 159 | ;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) 160 | ;;; using [public_key] (also represented by a 256-bit unsigned integer). 161 | ;;; The signature must contain at least 512 data bits; only the first 512 bits are used. 162 | ;;; The result is `−1` if the signature is valid, `0` otherwise. 163 | ;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. 164 | ;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, 165 | ;;; the second hashing occurring inside `CHKSIGNS`. 166 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 167 | 168 | ;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, 169 | ;;; similarly to [check_signature]. 170 | ;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. 171 | ;;; The verification of Ed25519 signatures is the standard one, 172 | ;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. 173 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 174 | 175 | {--- 176 | # Computation of boc size 177 | The primitives below may be useful for computing storage fees of user-provided data. 178 | -} 179 | 180 | ;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. 181 | ;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` 182 | ;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account 183 | ;;; the identification of equal cells. 184 | ;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, 185 | ;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. 186 | ;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; 187 | ;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and 188 | ;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. 189 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 190 | 191 | ;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. 192 | ;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; 193 | ;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. 194 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 195 | 196 | ;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. 197 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 198 | 199 | ;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. 200 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 201 | 202 | ;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) 203 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 204 | 205 | {-- 206 | # Debug primitives 207 | Only works for local TVM execution with debug level verbosity 208 | -} 209 | ;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. 210 | () dump_stack() impure asm "DUMPSTK"; 211 | 212 | {- 213 | # Persistent storage save and load 214 | -} 215 | 216 | ;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. 217 | cell get_data() asm "c4 PUSH"; 218 | 219 | ;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. 220 | () set_data(cell c) impure asm "c4 POP"; 221 | 222 | {- 223 | # Continuation primitives 224 | -} 225 | ;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. 226 | ;;; The primitive returns the current value of `c3`. 227 | cont get_c3() impure asm "c3 PUSH"; 228 | 229 | ;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. 230 | ;;; Note that after execution of this primitive the current code 231 | ;;; (and the stack of recursive function calls) won't change, 232 | ;;; but any other function call will use a function from the new code. 233 | () set_c3(cont c) impure asm "c3 POP"; 234 | 235 | ;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. 236 | cont bless(slice s) impure asm "BLESS"; 237 | 238 | {--- 239 | # Gas related primitives 240 | -} 241 | 242 | ;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, 243 | ;;; decreasing the value of `gr` by `gc` in the process. 244 | ;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. 245 | ;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. 246 | ;;; 247 | ;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). 248 | () accept_message() impure asm "ACCEPT"; 249 | 250 | ;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. 251 | ;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, 252 | ;;; an (unhandled) out of gas exception is thrown before setting new gas limits. 253 | ;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. 254 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 255 | 256 | ;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) 257 | ;;; so that the current execution is considered “successful” with the saved values even if an exception 258 | ;;; in Computation Phase is thrown later. 259 | () commit() impure asm "COMMIT"; 260 | 261 | ;;; Not implemented 262 | ;;; Computes the amount of gas that can be bought for `amount` nanoTONs, 263 | ;;; and sets `gl` accordingly in the same way as [set_gas_limit]. 264 | ;;() buy_gas(int amount) impure asm "BUYGAS"; 265 | 266 | ;;; Computes the minimum of two integers [x] and [y]. 267 | int min(int x, int y) asm "MIN"; 268 | 269 | ;;; Computes the maximum of two integers [x] and [y]. 270 | int max(int x, int y) asm "MAX"; 271 | 272 | ;;; Sorts two integers. 273 | (int, int) minmax(int x, int y) asm "MINMAX"; 274 | 275 | ;;; Computes the absolute value of an integer [x]. 276 | int abs(int x) asm "ABS"; 277 | 278 | {- 279 | # Slice primitives 280 | 281 | It is said that a primitive _loads_ some data, 282 | if it returns the data and the remainder of the slice 283 | (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). 284 | 285 | It is said that a primitive _preloads_ some data, if it returns only the data 286 | (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). 287 | 288 | Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. 289 | -} 290 | 291 | 292 | ;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, 293 | ;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) 294 | ;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. 295 | slice begin_parse(cell c) asm "CTOS"; 296 | 297 | ;;; Checks if [s] is empty. If not, throws an exception. 298 | () end_parse(slice s) impure asm "ENDS"; 299 | 300 | ;;; Loads the first reference from the slice. 301 | (slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF"; 302 | 303 | ;;; Preloads the first reference from the slice. 304 | cell preload_ref(slice s) asm "PLDREF"; 305 | 306 | {- Functions below are commented because are implemented on compilator level for optimisation -} 307 | 308 | ;;; Loads a signed [len]-bit integer from a slice [s]. 309 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 310 | 311 | ;;; Loads an unsigned [len]-bit integer from a slice [s]. 312 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 313 | 314 | ;;; Preloads a signed [len]-bit integer from a slice [s]. 315 | ;; int preload_int(slice s, int len) asm "PLDIX"; 316 | 317 | ;;; Preloads an unsigned [len]-bit integer from a slice [s]. 318 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 319 | 320 | ;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 321 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 322 | 323 | ;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 324 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 325 | 326 | ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). 327 | (slice, int) load_grams(slice s) asm(-> 1 0) "LDGRAMS"; 328 | (slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16"; 329 | 330 | ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 331 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 332 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 333 | 334 | ;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 335 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 336 | 337 | ;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 338 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 339 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 340 | 341 | ;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 342 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 343 | 344 | ;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. 345 | ;;; (returns `null` if `nothing` constructor is used). 346 | (slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT"; 347 | 348 | ;;; Preloads a dictionary `D` from `slice` [s]. 349 | cell preload_dict(slice s) asm "PLDDICT"; 350 | 351 | ;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. 352 | slice skip_dict(slice s) asm "SKIPDICT"; 353 | (slice, ()) ~skip_dict(slice s) asm "SKIPDICT"; 354 | 355 | ;;; Loads (Maybe ^Cell) from `slice` [s]. 356 | ;;; In other words loads 1 bit and if it is true 357 | ;;; loads first ref and return it with slice remainder 358 | ;;; otherwise returns `null` and slice remainder 359 | (slice, cell) load_maybe_ref(slice s) asm(-> 1 0) "LDOPTREF"; 360 | 361 | ;;; Preloads (Maybe ^Cell) from `slice` [s]. 362 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 363 | 364 | 365 | ;;; Returns the depth of `cell` [c]. 366 | ;;; If [c] has no references, then return `0`; 367 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. 368 | ;;; If [c] is a `null` instead of a cell, returns zero. 369 | int cell_depth(cell c) asm "CDEPTH"; 370 | 371 | 372 | {- 373 | # Slice size primitives 374 | -} 375 | 376 | ;;; Returns the number of references in `slice` [s]. 377 | int slice_refs(slice s) asm "SREFS"; 378 | 379 | ;;; Returns the number of data bits in `slice` [s]. 380 | int slice_bits(slice s) asm "SBITS"; 381 | 382 | ;;; Returns both the number of data bits and the number of references in `slice` [s]. 383 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 384 | 385 | ;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). 386 | int slice_empty?(slice s) asm "SEMPTY"; 387 | 388 | ;;; Checks whether `slice` [s] has no bits of data. 389 | int slice_data_empty?(slice s) asm "SDEMPTY"; 390 | 391 | ;;; Checks whether `slice` [s] has no references. 392 | int slice_refs_empty?(slice s) asm "SREMPTY"; 393 | 394 | ;;; Returns the depth of `slice` [s]. 395 | ;;; If [s] has no references, then returns `0`; 396 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. 397 | int slice_depth(slice s) asm "SDEPTH"; 398 | 399 | {- 400 | # Builder size primitives 401 | -} 402 | 403 | ;;; Returns the number of cell references already stored in `builder` [b] 404 | int builder_refs(builder b) asm "BREFS"; 405 | 406 | ;;; Returns the number of data bits already stored in `builder` [b]. 407 | int builder_bits(builder b) asm "BBITS"; 408 | 409 | ;;; Returns the depth of `builder` [b]. 410 | ;;; If no cell references are stored in [b], then returns 0; 411 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. 412 | int builder_depth(builder b) asm "BDEPTH"; 413 | 414 | {- 415 | # Builder primitives 416 | It is said that a primitive _stores_ a value `x` into a builder `b` 417 | if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. 418 | It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). 419 | 420 | All the primitives below first check whether there is enough space in the `builder`, 421 | and only then check the range of the value being serialized. 422 | -} 423 | 424 | ;;; Creates a new empty `builder`. 425 | builder begin_cell() asm "NEWC"; 426 | 427 | ;;; Converts a `builder` into an ordinary `cell`. 428 | cell end_cell(builder b) asm "ENDC"; 429 | 430 | ;;; Stores a reference to `cell` [c] into `builder` [b]. 431 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 432 | 433 | ;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. 434 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 435 | 436 | ;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. 437 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 438 | 439 | 440 | ;;; Stores `slice` [s] into `builder` [b] 441 | builder store_slice(builder b, slice s) asm "STSLICER"; 442 | 443 | ;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. 444 | ;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, 445 | ;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, 446 | ;;; followed by an `8l`-bit unsigned big-endian representation of [x]. 447 | ;;; If [x] does not belong to the supported range, a range check exception is thrown. 448 | ;;; 449 | ;;; Store amounts of TonCoins to the builder as VarUInteger 16 450 | builder store_grams(builder b, int x) asm "STGRAMS"; 451 | builder store_coins(builder b, int x) asm "STVARUINT16"; 452 | 453 | ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. 454 | ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. 455 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 456 | 457 | ;;; Stores (Maybe ^Cell) to builder: 458 | ;;; if cell is null store 1 zero bit 459 | ;;; otherwise store 1 true bit and ref to cell 460 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 461 | 462 | 463 | {- 464 | # Address manipulation primitives 465 | The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: 466 | ```TL-B 467 | addr_none$00 = MsgAddressExt; 468 | addr_extern$01 len:(## 8) external_address:(bits len) 469 | = MsgAddressExt; 470 | anycast_info$_ depth:(#<= 30) { depth >= 1 } 471 | rewrite_pfx:(bits depth) = Anycast; 472 | addr_std$10 anycast:(Maybe Anycast) 473 | workchain_id:int8 address:bits256 = MsgAddressInt; 474 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) 475 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt; 476 | _ _:MsgAddressInt = MsgAddress; 477 | _ _:MsgAddressExt = MsgAddress; 478 | 479 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool 480 | src:MsgAddress dest:MsgAddressInt 481 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams 482 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 483 | ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt 484 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 485 | ``` 486 | A deserialized `MsgAddress` is represented by a tuple `t` as follows: 487 | 488 | - `addr_none` is represented by `t = (0)`, 489 | i.e., a tuple containing exactly one integer equal to zero. 490 | - `addr_extern` is represented by `t = (1, s)`, 491 | where slice `s` contains the field `external_address`. In other words, ` 492 | t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. 493 | - `addr_std` is represented by `t = (2, u, x, s)`, 494 | where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). 495 | Next, integer `x` is the `workchain_id`, and slice `s` contains the address. 496 | - `addr_var` is represented by `t = (3, u, x, s)`, 497 | where `u`, `x`, and `s` have the same meaning as for `addr_std`. 498 | -} 499 | 500 | ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, 501 | ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. 502 | (slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR"; 503 | 504 | ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. 505 | ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. 506 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 507 | 508 | ;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), 509 | ;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, 510 | ;;; and returns both the workchain and the 256-bit address as integers. 511 | ;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, 512 | ;;; throws a cell deserialization exception. 513 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 514 | 515 | ;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], 516 | ;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). 517 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 518 | 519 | {- 520 | # Dictionary primitives 521 | -} 522 | 523 | 524 | ;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), 525 | ;;; and returns the resulting dictionary. 526 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 527 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 528 | 529 | ;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), 530 | ;;; and returns the resulting dictionary. 531 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 532 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 533 | 534 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 535 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; 536 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; 537 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 538 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 539 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 540 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 541 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 542 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 543 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 544 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 545 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 546 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 547 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 548 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 549 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 550 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 551 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 552 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 553 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 554 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 555 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 556 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 557 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 558 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 559 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 560 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 561 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 562 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 563 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 564 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 565 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 566 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 567 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 568 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 569 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 570 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 571 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 572 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 573 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 574 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 575 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 576 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 577 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 578 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 579 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 580 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 581 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 582 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 583 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 584 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 585 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 586 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 587 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 588 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 589 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 590 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 591 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 592 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 593 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 594 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 595 | 596 | ;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL 597 | cell new_dict() asm "NEWDICT"; 598 | ;;; Checks whether a dictionary is empty. Equivalent to cell_null?. 599 | int dict_empty?(cell c) asm "DICTEMPTY"; 600 | 601 | 602 | {- Prefix dictionary primitives -} 603 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 604 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 605 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 606 | 607 | ;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. 608 | cell config_param(int x) asm "CONFIGOPTPARAM"; 609 | ;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. 610 | int cell_null?(cell c) asm "ISNULL"; 611 | 612 | ;;; 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. 613 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 614 | ;;; 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. 615 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 616 | ;;; 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. 617 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 618 | ;;; 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 619 | () set_code(cell new_code) impure asm "SETCODE"; 620 | 621 | ;;; 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. 622 | int random() impure asm "RANDU256"; 623 | ;;; 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. 624 | int rand(int range) impure asm "RAND"; 625 | ;;; Returns the current random seed as an unsigned 256-bit Integer. 626 | int get_seed() impure asm "RANDSEED"; 627 | ;;; Sets the random seed to unsigned 256-bit seed. 628 | () set_seed(int x) impure asm "SETRAND"; 629 | ;;; 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. 630 | () randomize(int x) impure asm "ADDRAND"; 631 | ;;; Equivalent to randomize(cur_lt());. 632 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 633 | 634 | ;;; Checks whether the data parts of two slices coinside 635 | int equal_slices_bits(slice a, slice b) asm "SDEQ"; 636 | ;;; Checks whether b is a null. Note, that FunC also has polymorphic null? built-in. 637 | int builder_null?(builder b) asm "ISNULL"; 638 | ;;; Concatenates two builders 639 | builder store_builder(builder to, builder from) asm "STBR"; 640 | 641 | ;; CUSTOM: 642 | 643 | ;; TVM UPGRADE 2023-07 https://docs.ton.org/learn/tvm-instructions/tvm-upgrade-2023-07 644 | ;; In mainnet since 20 Dec 2023 https://t.me/tonblockchain/226 645 | 646 | ;;; Retrieves code of smart-contract from c7 647 | cell my_code() asm "MYCODE"; 648 | 649 | ;;; Creates an output action and returns a fee for creating a message. Mode has the same effect as in the case of SENDRAWMSG 650 | int send_message(cell msg, int mode) impure asm "SENDMSG"; 651 | 652 | int gas_consumed() asm "GASCONSUMED"; 653 | 654 | ;; TVM V6 https://github.com/ton-blockchain/ton/blob/testnet/doc/GlobalVersions.md#version-6 655 | 656 | int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; 657 | int get_storage_fee(int workchain, int seconds, int bits, int cells) asm(cells bits seconds workchain) "GETSTORAGEFEE"; 658 | int get_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEE"; 659 | int get_precompiled_gas_consumption() asm "GETPRECOMPILEDGAS"; 660 | 661 | int get_simple_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEESIMPLE"; 662 | int get_simple_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEESIMPLE"; 663 | int get_original_fwd_fee(int workchain, int fwd_fee) asm(fwd_fee workchain) "GETORIGINALFWDFEE"; 664 | int my_storage_due() asm "DUEPAYMENT"; 665 | 666 | tuple get_fee_cofigs() asm "UNPACKEDCONFIGTUPLE"; 667 | 668 | ;; BASIC 669 | 670 | const int TRUE = -1; 671 | const int FALSE = 0; 672 | 673 | const int MASTERCHAIN = -1; 674 | const int BASECHAIN = 0; 675 | 676 | ;;; skip (Maybe ^Cell) from `slice` [s]. 677 | (slice, ()) ~skip_maybe_ref(slice s) asm "SKIPOPTREF"; 678 | 679 | (slice, int) ~load_bool(slice s) inline { 680 | return s.load_int(1); 681 | } 682 | 683 | builder store_bool(builder b, int value) inline { 684 | return b.store_int(value, 1); 685 | } 686 | 687 | ;; ADDRESS NONE 688 | ;; addr_none$00 = MsgAddressExt; https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L100 689 | 690 | builder store_address_none(builder b) inline { 691 | return b.store_uint(0, 2); 692 | } 693 | 694 | slice address_none() asm " = 0 } { bit_number < 1023 } = QueryId; 6 | 7 | // highload v3 8 | 9 | // crc32('internal_transfer n:# query_id:uint64 actions:^OutList n = InternalMsgBody n') = ae42e5a4 10 | 11 | internal_transfer#ae42e5a4 {n:#} query_id:uint64 actions:^(OutList n) = InternalMsgBody n; 12 | 13 | _ {n:#} subwallet_id:uint32 message_to_send:^Cell send_mode:uint8 query_id:QueryId created_at:uint64 timeout:uint22 = MsgInner; 14 | 15 | msg_body$_ {n:#} signature:bits512 ^(MsgInner) = ExternalInMsgBody; 16 | 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highload-wallet-v3", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "blueprint run", 6 | "build": "blueprint build --all", 7 | "test": "jest" 8 | }, 9 | "devDependencies": { 10 | "@ton/blueprint": "^0.18.0", 11 | "@ton/core": "^0.56.1", 12 | "@ton/crypto": "^3.2.0", 13 | "@ton/sandbox": "^0.16.0", 14 | "@ton/test-utils": "^0.4.2", 15 | "@ton/ton": "^13.9.0", 16 | "@types/jest": "^29.5.0", 17 | "@types/node": "^20.2.5", 18 | "jest": "^29.5.0", 19 | "prettier": "^3.1.0", 20 | "ts-jest": "^29.0.5", 21 | "ts-node": "^10.9.1", 22 | "typescript": "^5.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/HighloadWalletV3.spec.ts: -------------------------------------------------------------------------------- 1 | import { Blockchain, EmulationError, SandboxContract, createShardAccount, internal } from '@ton/sandbox'; 2 | import { beginCell, Cell, SendMode, toNano, Address, internal as internal_relaxed, Dictionary, BitString, OutActionSendMsg } from '@ton/core'; 3 | import {HighloadWalletV3, TIMEOUT_SIZE, TIMESTAMP_SIZE} from '../wrappers/HighloadWalletV3'; 4 | import '@ton/test-utils'; 5 | import { getSecureRandomBytes, KeyPair, keyPairFromSeed } from "ton-crypto"; 6 | import { randomBytes } from "crypto"; 7 | import {SUBWALLET_ID, Errors, DEFAULT_TIMEOUT, maxKeyCount, maxShift} from "./imports/const"; 8 | import { compile } from '@ton/blueprint'; 9 | import { getRandomInt } from '../utils'; 10 | import { findTransactionRequired, randomAddress } from '@ton/test-utils'; 11 | import { MsgGenerator } from '../wrappers/MsgGenerator'; 12 | import {HighloadQueryId} from "../wrappers/HighloadQueryId"; 13 | 14 | 15 | describe('HighloadWalletV3', () => { 16 | let keyPair: KeyPair; 17 | let code: Cell; 18 | 19 | let blockchain: Blockchain; 20 | let highloadWalletV3: SandboxContract; 21 | 22 | let shouldRejectWith: (p: Promise, code: number) => Promise; 23 | let getContractData: (address: Address) => Promise; 24 | let getContractCode: (address: Address) => Promise; 25 | 26 | beforeAll(async () => { 27 | keyPair = keyPairFromSeed(await getSecureRandomBytes(32)); 28 | code = await compile('HighloadWalletV3'); 29 | 30 | shouldRejectWith = async (p, code) => { 31 | try { 32 | await p; 33 | throw new Error(`Should throw ${code}`); 34 | } 35 | catch(e: unknown) { 36 | if(e instanceof EmulationError) { 37 | expect(e.exitCode !== undefined && e.exitCode == code).toBe(true); 38 | } 39 | else { 40 | throw e; 41 | } 42 | } 43 | } 44 | getContractData = async (address: Address) => { 45 | const smc = await blockchain.getContract(address); 46 | if(!smc.account.account) 47 | throw("Account not found") 48 | if(smc.account.account.storage.state.type != "active" ) 49 | throw("Atempting to get data on inactive account"); 50 | if(!smc.account.account.storage.state.state.data) 51 | throw("Data is not present"); 52 | return smc.account.account.storage.state.state.data 53 | } 54 | getContractCode = async (address: Address) => { 55 | const smc = await blockchain.getContract(address); 56 | if(!smc.account.account) 57 | throw("Account not found") 58 | if(smc.account.account.storage.state.type != "active" ) 59 | throw("Atempting to get code on inactive account"); 60 | if(!smc.account.account.storage.state.state.code) 61 | throw("Code is not present"); 62 | return smc.account.account.storage.state.state.code; 63 | } 64 | 65 | }); 66 | 67 | beforeEach(async () => { 68 | blockchain = await Blockchain.create(); 69 | blockchain.now = 1000; 70 | // blockchain.verbosity = { 71 | // print: true, 72 | // blockchainLogs: true, 73 | // vmLogs: 'vm_logs', 74 | // debugLogs: true, 75 | // } 76 | 77 | highloadWalletV3 = blockchain.openContract( 78 | HighloadWalletV3.createFromConfig( 79 | { 80 | publicKey: keyPair.publicKey, 81 | subwalletId: SUBWALLET_ID, 82 | timeout: DEFAULT_TIMEOUT 83 | }, 84 | code 85 | ) 86 | ); 87 | 88 | const deployer = await blockchain.treasury('deployer'); 89 | 90 | const deployResult = await highloadWalletV3.sendDeploy(deployer.getSender(), toNano('999999')); 91 | 92 | expect(deployResult.transactions).toHaveTransaction({ 93 | from: deployer.address, 94 | to: highloadWalletV3.address, 95 | deploy: true 96 | }); 97 | }); 98 | 99 | it('should deploy', async () => { 100 | expect(await highloadWalletV3.getPublicKey()).toEqual(keyPair.publicKey); 101 | }); 102 | 103 | it('should pass check sign', async () => { 104 | try { 105 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: HighloadQueryId.fromQueryId(0n), value: 0n}) 106 | const rndShift = getRandomInt(0, maxShift); 107 | const rndBitNum = getRandomInt(0, 1022); 108 | 109 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 110 | 111 | const testResult = await highloadWalletV3.sendExternalMessage( 112 | keyPair.secretKey, 113 | { 114 | createdAt: 1000, 115 | query_id: queryId, 116 | message, 117 | mode: 128, 118 | subwalletId: SUBWALLET_ID, 119 | timeout: DEFAULT_TIMEOUT 120 | } 121 | ); 122 | 123 | expect(testResult.transactions).toHaveTransaction({ 124 | from: highloadWalletV3.address, 125 | to: highloadWalletV3.address, 126 | success: true, 127 | }); 128 | } catch (e: any) { 129 | console.log(e.vmLogs) 130 | throw(e); 131 | } 132 | 133 | }); 134 | 135 | 136 | it('should fail check sign', async () => { 137 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: HighloadQueryId.fromQueryId(0n), value: 0n}) 138 | 139 | let badKey: Buffer; 140 | // Just in case we win a lotto 141 | do { 142 | badKey = randomBytes(64); 143 | } while(badKey.equals(keyPair.secretKey)); 144 | 145 | const rndShift = getRandomInt(0, maxShift); 146 | const rndBitNum = getRandomInt(0, 1022); 147 | 148 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 149 | 150 | await shouldRejectWith(highloadWalletV3.sendExternalMessage( 151 | badKey, 152 | { 153 | createdAt: 1000, 154 | query_id: queryId, 155 | message, 156 | mode: 128, 157 | subwalletId: SUBWALLET_ID, 158 | timeout: DEFAULT_TIMEOUT 159 | } 160 | ), Errors.invalid_signature) 161 | }); 162 | 163 | it('should fail subwallet check', async () => { 164 | let badSubwallet; 165 | 166 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: HighloadQueryId.fromQueryId(0n), value: 0n}) 167 | const curSubwallet= await highloadWalletV3.getSubwalletId(); 168 | expect(curSubwallet).toEqual(SUBWALLET_ID); 169 | 170 | const rndShift = getRandomInt(0, maxShift); 171 | const rndBitNum = getRandomInt(0, 1022); 172 | 173 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 174 | 175 | do { 176 | badSubwallet = getRandomInt(0, 1000); 177 | } while(badSubwallet == curSubwallet); 178 | 179 | await shouldRejectWith(highloadWalletV3.sendExternalMessage( 180 | keyPair.secretKey, 181 | { 182 | createdAt: 1000, 183 | query_id: queryId, 184 | mode: 128, 185 | message, 186 | subwalletId: badSubwallet, 187 | timeout: DEFAULT_TIMEOUT 188 | }), Errors.invalid_subwallet); 189 | }); 190 | it('should fail check created time', async () => { 191 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: HighloadQueryId.fromQueryId(0n), value: 0n}) 192 | 193 | const curTimeout = await highloadWalletV3.getTimeout(); 194 | 195 | const rndShift = getRandomInt(0, maxShift); 196 | const rndBitNum = getRandomInt(0, 1022); 197 | 198 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 199 | 200 | 201 | await shouldRejectWith(highloadWalletV3.sendExternalMessage( 202 | keyPair.secretKey, 203 | { 204 | createdAt: 1000 - getRandomInt(curTimeout + 1, curTimeout + 200), 205 | query_id: queryId, 206 | message, 207 | mode: 128, 208 | subwalletId: SUBWALLET_ID, 209 | timeout: DEFAULT_TIMEOUT 210 | } 211 | ), Errors.invalid_creation_time); 212 | }); 213 | 214 | it('should fail check query_id in actual queries', async () => { 215 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: new HighloadQueryId(), value: 0n}) 216 | 217 | const rndShift = getRandomInt(0, maxShift); 218 | const rndBitNum = getRandomInt(0, 1022); 219 | 220 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 221 | 222 | const testResult = await highloadWalletV3.sendExternalMessage( 223 | keyPair.secretKey, 224 | { 225 | createdAt: 1000, 226 | query_id: queryId, 227 | message, 228 | mode: 128, 229 | subwalletId: SUBWALLET_ID, 230 | timeout: DEFAULT_TIMEOUT 231 | } 232 | ); 233 | expect(testResult.transactions).toHaveTransaction({ 234 | from: highloadWalletV3.address, 235 | to: highloadWalletV3.address, 236 | success: true 237 | }); 238 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); 239 | 240 | await shouldRejectWith(highloadWalletV3.sendExternalMessage( 241 | keyPair.secretKey, 242 | { 243 | createdAt: 1000, 244 | query_id: queryId, 245 | message, 246 | mode: 128, 247 | subwalletId: SUBWALLET_ID, 248 | timeout: DEFAULT_TIMEOUT 249 | } 250 | ), Errors.already_executed); 251 | }); 252 | it('should work with max bitNumber = 1022', async () => { 253 | // bitNumber is a low part of 24 bit query_id 254 | const shift = getRandomInt(0, maxShift); 255 | 256 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(shift), BigInt(1022)); 257 | 258 | await expect(highloadWalletV3.sendExternalMessage( 259 | keyPair.secretKey, 260 | { 261 | createdAt: 1000, 262 | query_id: queryId, 263 | message: internal_relaxed({ 264 | to: randomAddress(0), 265 | value: 0n, 266 | }), 267 | mode: 0, 268 | subwalletId: SUBWALLET_ID, 269 | timeout: DEFAULT_TIMEOUT 270 | })).resolves.not.toThrow(EmulationError); 271 | }); 272 | 273 | it('should reject bitNumber = 1023', async () => { 274 | // bitNumber is a low part of 24 bit query_id 275 | const shift = getRandomInt(0, maxShift); 276 | const queryId = BigInt((shift << 10) + 1023); 277 | 278 | await expect(highloadWalletV3.sendExternalMessage( 279 | keyPair.secretKey, 280 | { 281 | createdAt: 1000, 282 | query_id: queryId, 283 | message: internal_relaxed({ 284 | to: randomAddress(0), 285 | value: 0n, 286 | }), 287 | mode: 0, 288 | subwalletId: SUBWALLET_ID, 289 | timeout: DEFAULT_TIMEOUT 290 | })).rejects.toThrow(EmulationError); 291 | }); 292 | it('should work with max shift = maxShift', async () => { 293 | // Shift is a high part of 24 bit query_id 294 | const rndBitNum = getRandomInt(0, 1022); 295 | const qIter = HighloadQueryId.fromShiftAndBitNumber(BigInt(maxShift), BigInt(rndBitNum)); 296 | await expect(highloadWalletV3.sendExternalMessage( 297 | keyPair.secretKey, 298 | { 299 | createdAt: 1000, 300 | query_id: qIter, 301 | message: internal_relaxed({ 302 | to: randomAddress(0), 303 | value: 0n, 304 | }), 305 | mode: 0, 306 | subwalletId: SUBWALLET_ID, 307 | timeout: DEFAULT_TIMEOUT 308 | })).resolves.not.toThrow(EmulationError); 309 | }); 310 | 311 | it('should fail check query_id in old queries', async () => { 312 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: new HighloadQueryId(), value: 0n}) 313 | 314 | const rndShift = getRandomInt(0, maxShift); 315 | const rndBitNum = getRandomInt(0, 1022); 316 | 317 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 318 | 319 | const testResult = await highloadWalletV3.sendExternalMessage( 320 | keyPair.secretKey, 321 | { 322 | createdAt: 1000, 323 | query_id: queryId, 324 | message, 325 | mode: 128, 326 | subwalletId: SUBWALLET_ID, 327 | timeout: DEFAULT_TIMEOUT 328 | } 329 | ); 330 | expect(testResult.transactions).toHaveTransaction({ 331 | from: highloadWalletV3.address, 332 | to: highloadWalletV3.address, 333 | success: true 334 | }); 335 | 336 | blockchain.now = 1000 + 100; 337 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); 338 | 339 | await shouldRejectWith(highloadWalletV3.sendExternalMessage( 340 | keyPair.secretKey, 341 | { 342 | createdAt: 1050, 343 | query_id: queryId, 344 | message, 345 | mode: 128, 346 | subwalletId: SUBWALLET_ID, 347 | timeout: DEFAULT_TIMEOUT 348 | } 349 | ), Errors.already_executed) 350 | }); 351 | 352 | it('should be cleared queries hashmaps', async () => { 353 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: new HighloadQueryId(), value: 0n}) 354 | 355 | const rndShift = getRandomInt(0, maxShift); 356 | const rndBitNum = getRandomInt(0, 1022); 357 | 358 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 359 | 360 | const testResult1 = await highloadWalletV3.sendExternalMessage( 361 | keyPair.secretKey, 362 | { 363 | createdAt: 1000, 364 | query_id: queryId, 365 | message, 366 | mode: 128, 367 | subwalletId: SUBWALLET_ID, 368 | timeout: DEFAULT_TIMEOUT 369 | } 370 | ); 371 | expect(testResult1.transactions).toHaveTransaction({ 372 | from: highloadWalletV3.address, 373 | to: highloadWalletV3.address, 374 | success: true 375 | }); 376 | 377 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); 378 | blockchain.now = 1000 + 260; 379 | // get_is_processed should account for query expiery 380 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(false); 381 | 382 | const newShift = getRandomInt(0, maxShift); 383 | const newBitNum = getRandomInt(0, 1022); 384 | 385 | const newQueryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(newShift), BigInt(newBitNum)); 386 | 387 | const testResult2 = await highloadWalletV3.sendExternalMessage( 388 | keyPair.secretKey, 389 | { 390 | createdAt: 1200, 391 | query_id: newQueryId, 392 | message, 393 | mode: 128, 394 | subwalletId: SUBWALLET_ID, 395 | timeout: DEFAULT_TIMEOUT 396 | } 397 | ); 398 | expect(testResult2.transactions).toHaveTransaction({ 399 | from: highloadWalletV3.address, 400 | to: highloadWalletV3.address, 401 | success: true 402 | }); 403 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(false); 404 | expect(await highloadWalletV3.getProcessed(newQueryId)).toBe(true); 405 | expect(await highloadWalletV3.getLastCleaned()).toEqual(testResult2.transactions[0].now); 406 | }); 407 | it('queries dictionary with max keys should fit in credit limit', async () => { 408 | // 2 ** 13 = 8192 keys 409 | // Artificial situation where both dict's get looked up 410 | const message = highloadWalletV3.createInternalTransfer({actions: [], queryId: new HighloadQueryId(), value: 0n}) 411 | const newQueries = Dictionary.empty(Dictionary.Keys.Uint(13), Dictionary.Values.Cell()); 412 | const padding = new BitString(Buffer.alloc(128, 0), 0, 1023 - 13); 413 | 414 | for(let i = 0; i < maxKeyCount; i++) { 415 | newQueries.set(i, beginCell().storeUint(i, 13).storeBits(padding).endCell()); 416 | } 417 | 418 | const oldQueries = Dictionary.empty(Dictionary.Keys.Uint(13), Dictionary.Values.Cell()); 419 | for(let i = 0; i < maxKeyCount; i++) { 420 | oldQueries.set(i, beginCell().storeBits(padding).storeUint(i, 13).endCell()); 421 | } 422 | 423 | const smc = await blockchain.getContract(highloadWalletV3.address); 424 | const walletState = await getContractData(highloadWalletV3.address); 425 | const ws = walletState.beginParse(); 426 | const head = ws.loadBits(256 + 32); // pubkey + subwallet 427 | const tail = ws.skip(2 + TIMESTAMP_SIZE).loadBits(TIMEOUT_SIZE); 428 | 429 | const newState = beginCell() 430 | .storeBits(head) 431 | .storeDict(oldQueries) 432 | .storeDict(newQueries) 433 | .storeUint(blockchain.now!, TIMESTAMP_SIZE) // DO NOT CLEAN 434 | .storeBits(tail) 435 | .endCell(); 436 | 437 | await blockchain.setShardAccount(highloadWalletV3.address, createShardAccount({ 438 | address: highloadWalletV3.address, 439 | code, 440 | data: newState, 441 | balance: smc.balance, 442 | workchain: 0 443 | })); 444 | 445 | const rndShift = maxShift; 446 | const rndBitNum = 700; 447 | 448 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 449 | const res = highloadWalletV3.sendExternalMessage( 450 | keyPair.secretKey, 451 | { 452 | createdAt: 1000, 453 | query_id: queryId, 454 | message, 455 | mode: 128, 456 | subwalletId: SUBWALLET_ID, 457 | timeout: DEFAULT_TIMEOUT 458 | }); 459 | await expect(res).resolves.not.toThrow(); 460 | expect((await res).transactions).toHaveTransaction({ 461 | on: highloadWalletV3.address, 462 | aborted: false, 463 | outMessagesCount: 1 464 | }); 465 | }); 466 | it('should send internal message', async () => { 467 | const testAddr = randomAddress(0); 468 | const testBody = beginCell().storeUint(getRandomInt(0, 1000000), 32).endCell(); 469 | 470 | const rndShift = getRandomInt(0, maxShift); 471 | const rndBitNum = 1022; 472 | 473 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 474 | 475 | let res = await highloadWalletV3.sendExternalMessage( 476 | keyPair.secretKey, 477 | { 478 | query_id: queryId, 479 | message: internal_relaxed({ 480 | to: testAddr, 481 | bounce: false, 482 | value: toNano('123'), 483 | body: testBody 484 | }), 485 | createdAt: 1000, 486 | mode: SendMode.PAY_GAS_SEPARATELY, 487 | subwalletId: SUBWALLET_ID, 488 | timeout: DEFAULT_TIMEOUT 489 | }); 490 | expect(res.transactions).toHaveTransaction({ 491 | on: testAddr, 492 | from: highloadWalletV3.address, 493 | value: toNano('123'), 494 | body: testBody 495 | }); 496 | 497 | expect(await highloadWalletV3.getProcessed(queryId)).toBe(true); 498 | 499 | // second transfer rejected 500 | 501 | let fail = false; 502 | try { 503 | await highloadWalletV3.sendExternalMessage( 504 | keyPair.secretKey, 505 | { 506 | query_id: queryId, 507 | message: internal_relaxed({ 508 | to: testAddr, 509 | bounce: false, 510 | value: toNano('123'), 511 | body: testBody 512 | }), 513 | createdAt: 1000, 514 | mode: SendMode.PAY_GAS_SEPARATELY, 515 | subwalletId: SUBWALLET_ID, 516 | timeout: DEFAULT_TIMEOUT 517 | }); 518 | } catch (e) { 519 | fail = true; 520 | } 521 | 522 | expect(fail).toBe(true); 523 | }); 524 | it('should ignore set_code action', async () => { 525 | const mockCode = beginCell().storeUint(getRandomInt(0, 1000000), 32).endCell(); 526 | const testBody = beginCell().storeUint(getRandomInt(0, 1000000), 32).endCell(); 527 | const testAddr = randomAddress(); 528 | 529 | const rndShift = getRandomInt(0, maxShift); 530 | const rndBitNum = getRandomInt(0, 1022); 531 | 532 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 533 | 534 | // In case test suite is broken 535 | expect(await getContractCode(highloadWalletV3.address)).toEqualCell(code); 536 | const message = highloadWalletV3.createInternalTransfer({ 537 | actions: [{ 538 | type: 'setCode', 539 | newCode: mockCode 540 | }, 541 | { 542 | type: 'sendMsg', 543 | mode: SendMode.PAY_GAS_SEPARATELY, 544 | outMsg: internal_relaxed({ 545 | to: testAddr, 546 | value: toNano('0.1'), 547 | body: testBody 548 | }) 549 | }], 550 | queryId: HighloadQueryId.fromQueryId(123n), 551 | value: 0n 552 | }); 553 | 554 | const res = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { 555 | createdAt: 1000, 556 | query_id: queryId, 557 | message, 558 | mode: 128, 559 | subwalletId: SUBWALLET_ID, 560 | timeout: DEFAULT_TIMEOUT 561 | }); 562 | 563 | // Code should not change 564 | expect(await getContractCode(highloadWalletV3.address)).toEqualCell(code); 565 | // Rest of the action pack should execute 566 | expect(res.transactions).toHaveTransaction({ 567 | from: highloadWalletV3.address, 568 | to: testAddr, 569 | body: testBody, 570 | value: toNano('0.1') 571 | }); 572 | }); 573 | it('should send external message', async () => { 574 | const testBody = beginCell().storeUint(getRandomInt(0, 1000000), 32).endCell(); 575 | 576 | const rndShift = getRandomInt(0, maxShift); 577 | const rndBitNum = getRandomInt(0, 1022); 578 | 579 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 580 | 581 | const message = highloadWalletV3.createInternalTransfer({actions: [{ 582 | type: 'sendMsg', 583 | mode: SendMode.NONE, 584 | outMsg: { 585 | info: { 586 | type: 'external-out', 587 | createdAt: 0, 588 | createdLt: 0n 589 | }, 590 | body: testBody 591 | } 592 | }], queryId: new HighloadQueryId(), value: 0n}) 593 | const testResult = await highloadWalletV3.sendExternalMessage( 594 | keyPair.secretKey, 595 | { 596 | createdAt: 1000, 597 | query_id: queryId, 598 | message, 599 | mode: 128, 600 | subwalletId: SUBWALLET_ID, 601 | timeout: DEFAULT_TIMEOUT 602 | } 603 | ); 604 | 605 | const sentTx = findTransactionRequired(testResult.transactions, { 606 | from: highloadWalletV3.address, 607 | to: highloadWalletV3.address, 608 | success: true, 609 | outMessagesCount: 1, 610 | actionResultCode: 0 611 | }); 612 | 613 | expect(sentTx.externals.length).toBe(1); 614 | expect(sentTx.externals[0].body).toEqualCell(testBody); 615 | 616 | const processed = await highloadWalletV3.getProcessed(queryId); 617 | expect(processed).toBe(true); 618 | }); 619 | it('should handle 254 actions in one go', async () => { 620 | const curQuery = new HighloadQueryId(); 621 | let outMsgs: OutActionSendMsg[] = new Array(254); 622 | 623 | for(let i = 0; i < 254; i++) { 624 | outMsgs[i] = { 625 | type: 'sendMsg', 626 | mode: SendMode.NONE, 627 | outMsg: internal_relaxed({ 628 | to: randomAddress(), 629 | value: toNano('0.015'), 630 | body: beginCell().storeUint(i, 32).endCell() 631 | }), 632 | } 633 | } 634 | 635 | const res = await highloadWalletV3.sendBatch(keyPair.secretKey, outMsgs, SUBWALLET_ID, curQuery, DEFAULT_TIMEOUT, 1000); 636 | 637 | expect(res.transactions).toHaveTransaction({ 638 | on: highloadWalletV3.address, 639 | outMessagesCount: 254 640 | }); 641 | for(let i = 0; i < 254; i++) { 642 | expect(res.transactions).toHaveTransaction({ 643 | from: highloadWalletV3.address, 644 | body: outMsgs[i].outMsg.body 645 | }) 646 | } 647 | expect(await highloadWalletV3.getProcessed(curQuery)).toBe(true); 648 | }); 649 | it('should be able to go beyond 255 messages with chained internal_transfer', async () => { 650 | const msgCount = getRandomInt(256, 507); 651 | const msgs : OutActionSendMsg[] = new Array(msgCount); 652 | const curQuery = new HighloadQueryId(); 653 | 654 | for(let i = 0; i < msgCount; i++) { 655 | msgs[i] = { 656 | type: 'sendMsg', 657 | mode: SendMode.PAY_GAS_SEPARATELY, 658 | outMsg: internal_relaxed({ 659 | to: randomAddress(0), 660 | value: toNano('0.015'), 661 | body: beginCell().storeUint(i, 32).endCell() 662 | }) 663 | }; 664 | } 665 | 666 | const res = await highloadWalletV3.sendBatch(keyPair.secretKey, msgs, SUBWALLET_ID, curQuery, DEFAULT_TIMEOUT, 1000); 667 | 668 | expect(res.transactions).toHaveTransaction({ 669 | on: highloadWalletV3.address, 670 | outMessagesCount: 254 671 | }); 672 | expect(res.transactions).toHaveTransaction({ 673 | on: highloadWalletV3.address, 674 | outMessagesCount: msgCount - 253 675 | }); 676 | for(let i = 0; i < msgCount; i++) { 677 | expect(res.transactions).toHaveTransaction({ 678 | from: highloadWalletV3.address, 679 | body: msgs[i].outMsg.body 680 | }); 681 | } 682 | expect(await highloadWalletV3.getProcessed(curQuery)).toBe(true); 683 | }); 684 | it('should ignore internal transfer from address different from self', async () => { 685 | const rndShift = getRandomInt(0, maxShift); 686 | const rndBitNum = getRandomInt(0, 1022); 687 | 688 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 689 | const testAddr = randomAddress(0); 690 | 691 | const transferBody = HighloadWalletV3.createInternalTransferBody({ 692 | queryId, 693 | actions: [{ 694 | type: 'sendMsg', 695 | mode: SendMode.PAY_GAS_SEPARATELY, 696 | outMsg: internal_relaxed({ 697 | to: testAddr, 698 | value: toNano('1000') 699 | }) 700 | }]}); 701 | 702 | let res = await blockchain.sendMessage(internal({ 703 | from: testAddr, 704 | to: highloadWalletV3.address, 705 | value: toNano('1'), 706 | body: transferBody 707 | })); 708 | 709 | expect(res.transactions).not.toHaveTransaction({ 710 | on: testAddr, 711 | from: highloadWalletV3.address, 712 | value: toNano('1000') 713 | }); 714 | 715 | // Make sure we failed because of source address 716 | 717 | res = await blockchain.sendMessage(internal({ 718 | from: highloadWalletV3.address, // Self 719 | to: highloadWalletV3.address, 720 | value: toNano('1'), 721 | body: transferBody 722 | })); 723 | expect(res.transactions).toHaveTransaction({ 724 | on: testAddr, 725 | from: highloadWalletV3.address, 726 | value: toNano('1000') 727 | }); 728 | }); 729 | it('should ignore bounced messages', async () => { 730 | 731 | const rndShift = getRandomInt(0, maxShift); 732 | const rndBitNum = getRandomInt(0, 1022); 733 | 734 | const queryId = HighloadQueryId.fromShiftAndBitNumber(BigInt(rndShift), BigInt(rndBitNum)); 735 | const testAddr = randomAddress(0); 736 | 737 | const transferBody = HighloadWalletV3.createInternalTransferBody({ 738 | queryId, 739 | actions: [{ 740 | type: 'sendMsg', 741 | mode: SendMode.PAY_GAS_SEPARATELY, 742 | outMsg: internal_relaxed({ 743 | to: testAddr, 744 | value: toNano('1000') 745 | }) 746 | }]}); 747 | // Let's be tricky and send bounced message from trusted address 748 | let res = await blockchain.sendMessage(internal({ 749 | from: highloadWalletV3.address, 750 | to: highloadWalletV3.address, 751 | body: transferBody, 752 | value: toNano('1'), 753 | bounced: true 754 | })); 755 | 756 | expect(res.transactions).not.toHaveTransaction({ 757 | on: testAddr, 758 | from: highloadWalletV3.address, 759 | value: toNano('1000') 760 | }); 761 | 762 | // Make sure we failed because of bounced flag 763 | 764 | res = await blockchain.sendMessage(internal({ 765 | from: highloadWalletV3.address, 766 | to: highloadWalletV3.address, 767 | body: transferBody, 768 | value: toNano('1'), 769 | bounced: false 770 | })); 771 | 772 | expect(res.transactions).toHaveTransaction({ 773 | on: testAddr, 774 | from: highloadWalletV3.address, 775 | value: toNano('1000') 776 | }); 777 | }); 778 | it('should ignore invalid message in payload', async () => { 779 | const testAddr = randomAddress(0); 780 | const badGenerator = new MsgGenerator(0); 781 | let queryIter = new HighloadQueryId(); 782 | 783 | for(let badMsg of badGenerator.generateBadMsg()) { 784 | const res = await highloadWalletV3.sendExternalMessage( 785 | keyPair.secretKey, 786 | { 787 | mode: SendMode.NONE, 788 | message: badMsg, 789 | query_id: queryIter, 790 | subwalletId: SUBWALLET_ID, 791 | createdAt: 1000, 792 | timeout: DEFAULT_TIMEOUT 793 | }); 794 | expect(res.transactions).toHaveTransaction({ 795 | on: highloadWalletV3.address, 796 | success: true, // Compute phase has to succeed 797 | outMessagesCount: 0 798 | }); 799 | // Expect query to be processed 800 | expect(await highloadWalletV3.getProcessed(queryIter)).toBe(true); 801 | queryIter = queryIter.getNext(); 802 | } 803 | }); 804 | it('timeout replay attack', async () => { 805 | /* 806 | * Timeout is not part of the external 807 | * So in theory one could deploy contract with 808 | * different timeout without thinking too much. 809 | * This opens up avenue for replay attack. 810 | * So, at every deploy one should always change key or subwallet id 811 | */ 812 | const deployer = await blockchain.treasury('new_deployer'); 813 | const attacker = await blockchain.treasury('attacker'); 814 | 815 | // Same contract different timeout 816 | const newWallet = blockchain.openContract( 817 | HighloadWalletV3.createFromConfig( 818 | { 819 | publicKey: keyPair.publicKey, 820 | subwalletId: SUBWALLET_ID, 821 | timeout: 1234, 822 | }, 823 | code 824 | ) 825 | ); 826 | 827 | let res = await newWallet.sendDeploy(deployer.getSender(), toNano('1000')); 828 | expect(res.transactions).toHaveTransaction({ 829 | on: newWallet.address, 830 | deploy: true, 831 | success: true 832 | }); 833 | 834 | // So attacker requested legit withdraw on the exchange 835 | const legitResp = await highloadWalletV3.sendExternalMessage(keyPair.secretKey, { 836 | createdAt: 1000, 837 | query_id: new HighloadQueryId(), 838 | mode: SendMode.PAY_GAS_SEPARATELY, 839 | subwalletId: SUBWALLET_ID, 840 | timeout: DEFAULT_TIMEOUT, 841 | message: internal_relaxed({ 842 | to: attacker.address, 843 | value: toNano('10') 844 | }) 845 | }); 846 | 847 | const legitTx = findTransactionRequired(legitResp.transactions, { 848 | on: highloadWalletV3.address, 849 | outMessagesCount: 1 850 | }); 851 | 852 | expect(legitResp.transactions).toHaveTransaction({ 853 | on: attacker.address, 854 | value: toNano('10') 855 | }); 856 | 857 | // And now can replay it on contract with different timeout 858 | const replyExt = legitTx.inMessage!; 859 | if(replyExt.info.type !== 'external-in') { 860 | throw TypeError("No way"); 861 | } 862 | // Replace dest 863 | replyExt.info = { 864 | ...replyExt.info, 865 | dest: newWallet.address 866 | }; 867 | let fail = false; 868 | try { 869 | const reply = await blockchain.sendMessage(replyExt); 870 | } catch (e) { 871 | fail = true; 872 | } 873 | 874 | expect(fail).toBe(true); 875 | }); 876 | it('should work replay protection, but dont send message', async () => { 877 | const testResult = await highloadWalletV3.sendExternalMessage( 878 | keyPair.secretKey, 879 | { 880 | createdAt: 1000, 881 | query_id: new HighloadQueryId(), 882 | message: beginCell().storeUint(239, 17).endCell(), 883 | mode: 2, 884 | subwalletId: SUBWALLET_ID, 885 | timeout: DEFAULT_TIMEOUT 886 | } 887 | ); 888 | 889 | expect(testResult.transactions).toHaveTransaction({ 890 | to: highloadWalletV3.address, 891 | success: true, 892 | outMessagesCount: 0, 893 | actionResultCode: 0 894 | }); 895 | }); 896 | }); 897 | -------------------------------------------------------------------------------- /tests/imports/const.ts: -------------------------------------------------------------------------------- 1 | export const SUBWALLET_ID = 239; 2 | 3 | export const DEFAULT_TIMEOUT = 128; 4 | 5 | export enum OP { 6 | InternalTransfer = 0xae42e5a4 7 | } 8 | export abstract class Errors { 9 | static invalid_signature = 33; 10 | static invalid_subwallet = 34; 11 | static invalid_creation_time = 35; 12 | static already_executed = 36; 13 | } 14 | 15 | export const maxKeyCount = (1 << 13); //That is max key count not max key value 16 | export const maxShift = maxKeyCount - 1; 17 | export const maxQueryCount = maxKeyCount * 1023; // Therefore value count 18 | export const maxQueryId = (maxShift << 10) + 1022; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | const getRandom = (min:number, max:number) => { 2 | return Math.random() * (max - min) + min; 3 | } 4 | 5 | export const getRandomInt = (min: number, max: number) => { 6 | return Math.round(getRandom(min, max)); 7 | } 8 | -------------------------------------------------------------------------------- /wrappers/HighloadQueryId.ts: -------------------------------------------------------------------------------- 1 | const BIT_NUMBER_SIZE = 10n; // 10 bit 2 | const SHIFT_SIZE = 13n; // 13 bit 3 | const MAX_BIT_NUMBER = 1022n; 4 | const MAX_SHIFT = 8191n; // 2^13 = 8192 5 | 6 | export class HighloadQueryId { 7 | private shift: bigint; // [0 .. 8191] 8 | private bitnumber: bigint; // [0 .. 1022] 9 | 10 | constructor() { 11 | this.shift = 0n; 12 | this.bitnumber = 0n; 13 | } 14 | 15 | static fromShiftAndBitNumber(shift: bigint, bitnumber: bigint): HighloadQueryId { 16 | const q = new HighloadQueryId(); 17 | q.shift = shift; 18 | if (q.shift < 0) throw new Error('invalid shift'); 19 | if (q.shift > MAX_SHIFT) throw new Error('invalid shift'); 20 | q.bitnumber = bitnumber; 21 | if (q.bitnumber < 0) throw new Error('invalid bitnumber'); 22 | if (q.bitnumber > MAX_BIT_NUMBER) throw new Error('invalid bitnumber'); 23 | return q; 24 | } 25 | 26 | 27 | getNext() { 28 | let newBitnumber = this.bitnumber + 1n; 29 | let newShift = this.shift; 30 | 31 | if (newShift === MAX_SHIFT && newBitnumber > (MAX_BIT_NUMBER - 1n)) { 32 | throw new Error('Overload'); // NOTE: we left one queryId for emergency withdraw 33 | } 34 | 35 | if (newBitnumber > MAX_BIT_NUMBER) { 36 | newBitnumber = 0n; 37 | newShift += 1n; 38 | if (newShift > MAX_SHIFT) { 39 | throw new Error('Overload') 40 | } 41 | } 42 | 43 | return HighloadQueryId.fromShiftAndBitNumber(newShift, newBitnumber); 44 | } 45 | 46 | hasNext() { 47 | const isEnd = this.bitnumber >= (MAX_BIT_NUMBER - 1n) && this.shift === MAX_SHIFT; // NOTE: we left one queryId for emergency withdraw; 48 | return !isEnd; 49 | } 50 | 51 | getShift(): bigint { 52 | return this.shift; 53 | } 54 | 55 | getBitNumber(): bigint { 56 | return this.bitnumber; 57 | } 58 | 59 | getQueryId(): bigint { 60 | return (this.shift << BIT_NUMBER_SIZE) + this.bitnumber; 61 | } 62 | 63 | static fromQueryId(queryId: bigint): HighloadQueryId { 64 | const shift = queryId >> BIT_NUMBER_SIZE; 65 | const bitnumber = queryId & 1023n; 66 | return this.fromShiftAndBitNumber(shift, bitnumber); 67 | } 68 | 69 | static fromSeqno(i: bigint): HighloadQueryId { 70 | const shift = i / 1023n; 71 | const bitnumber = i % 1023n; 72 | return this.fromShiftAndBitNumber(shift, bitnumber); 73 | } 74 | 75 | /** 76 | * @return {bigint} [0 .. 8380415] 77 | */ 78 | toSeqno(): bigint { 79 | return this.bitnumber + this.shift * 1023n; 80 | } 81 | } -------------------------------------------------------------------------------- /wrappers/HighloadWalletV3.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | lang: 'func', 5 | targets: ['contracts/highload-wallet-v3.func'], 6 | }; -------------------------------------------------------------------------------- /wrappers/HighloadWalletV3.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | Contract, 6 | contractAddress, 7 | ContractProvider, 8 | internal as internal_relaxed, 9 | MessageRelaxed, 10 | OutAction, 11 | OutActionSendMsg, 12 | Sender, 13 | SendMode, 14 | storeMessageRelaxed, 15 | storeOutList, 16 | toNano 17 | } from '@ton/core'; 18 | // import { hex as CodeHex } from '../build/HighloadWalletV3.compiled.json'; 19 | import {sign} from "ton-crypto"; 20 | import {OP} from "../tests/imports/const"; 21 | import {HighloadQueryId} from "./HighloadQueryId"; 22 | 23 | // export const HighloadWalletV3Code = Cell.fromBoc(Buffer.from(CodeHex, "hex"))[0] 24 | 25 | export type HighloadWalletV3Config = { 26 | publicKey: Buffer, 27 | subwalletId: number, 28 | timeout: number 29 | }; 30 | 31 | 32 | export const TIMESTAMP_SIZE = 64; 33 | export const TIMEOUT_SIZE = 22; 34 | 35 | export function highloadWalletV3ConfigToCell(config: HighloadWalletV3Config): Cell { 36 | return beginCell() 37 | .storeBuffer(config.publicKey) 38 | .storeUint(config.subwalletId, 32) 39 | .storeUint(0, 1 + 1 + TIMESTAMP_SIZE) 40 | .storeUint(config.timeout, TIMEOUT_SIZE) 41 | .endCell(); 42 | } 43 | 44 | export class HighloadWalletV3 implements Contract { 45 | 46 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { 47 | } 48 | 49 | static createFromAddress(address: Address) { 50 | return new HighloadWalletV3(address); 51 | } 52 | 53 | static createFromConfig(config: HighloadWalletV3Config, code: Cell, workchain = 0) { 54 | const data = highloadWalletV3ConfigToCell(config); 55 | const init = {code, data}; 56 | return new HighloadWalletV3(contractAddress(workchain, init), init); 57 | } 58 | 59 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 60 | await provider.internal(via, { 61 | value, 62 | bounce: false, 63 | sendMode: SendMode.PAY_GAS_SEPARATELY, 64 | body: beginCell().endCell(), 65 | }); 66 | } 67 | 68 | async sendExternalMessage( 69 | provider: ContractProvider, 70 | secretKey: Buffer, 71 | opts: { 72 | message: MessageRelaxed | Cell, 73 | mode: number, 74 | query_id: bigint | HighloadQueryId, 75 | createdAt: number, 76 | subwalletId: number, 77 | timeout: number, 78 | } 79 | ) { 80 | let messageCell: Cell; 81 | 82 | if (opts.message instanceof Cell) { 83 | messageCell = opts.message 84 | } else { 85 | const messageBuilder = beginCell(); 86 | messageBuilder.store(storeMessageRelaxed(opts.message)) 87 | messageCell = messageBuilder.endCell(); 88 | } 89 | 90 | const queryId = (opts.query_id instanceof HighloadQueryId) ? opts.query_id.getQueryId() : opts.query_id; 91 | 92 | const messageInner = beginCell() 93 | .storeUint(opts.subwalletId, 32) 94 | .storeRef(messageCell) 95 | .storeUint(opts.mode, 8) 96 | .storeUint(queryId, 23) 97 | .storeUint(opts.createdAt, TIMESTAMP_SIZE) 98 | .storeUint(opts.timeout, TIMEOUT_SIZE) 99 | .endCell(); 100 | 101 | await provider.external( 102 | beginCell() 103 | .storeBuffer(sign(messageInner.hash(), secretKey)) 104 | .storeRef(messageInner) 105 | .endCell() 106 | ); 107 | } 108 | 109 | async sendBatch(provider: ContractProvider, secretKey: Buffer, messages: OutActionSendMsg[], subwallet: number, query_id: HighloadQueryId, timeout: number, createdAt?: number, value: bigint = 0n) { 110 | if (createdAt == undefined) { 111 | createdAt = Math.floor(Date.now() / 1000); 112 | } 113 | return await this.sendExternalMessage(provider, secretKey, { 114 | message: this.packActions(messages, value, query_id), 115 | mode: value > 0n ? SendMode.PAY_GAS_SEPARATELY : SendMode.CARRY_ALL_REMAINING_BALANCE, 116 | query_id: query_id, 117 | createdAt: createdAt, 118 | subwalletId: subwallet, 119 | timeout: timeout 120 | }); 121 | } 122 | 123 | static createInternalTransferBody(opts: { 124 | actions: OutAction[] | Cell, 125 | queryId: HighloadQueryId, 126 | }) { 127 | let actionsCell: Cell; 128 | if (opts.actions instanceof Cell) { 129 | actionsCell = opts.actions; 130 | } else { 131 | if (opts.actions.length > 254) { 132 | throw TypeError("Max allowed action count is 254. Use packActions instead."); 133 | } 134 | const actionsBuilder = beginCell(); 135 | storeOutList(opts.actions)(actionsBuilder); 136 | actionsCell = actionsBuilder.endCell(); 137 | } 138 | return beginCell().storeUint(OP.InternalTransfer, 32) 139 | .storeUint(opts.queryId.getQueryId(), 64) 140 | .storeRef(actionsCell) 141 | .endCell(); 142 | 143 | 144 | } 145 | 146 | createInternalTransfer(opts: { 147 | actions: OutAction[] | Cell 148 | queryId: HighloadQueryId, 149 | value: bigint 150 | }) { 151 | 152 | return internal_relaxed({ 153 | to: this.address, 154 | value: opts.value, 155 | body: HighloadWalletV3.createInternalTransferBody(opts) 156 | }); 157 | /*beginCell() 158 | .storeUint(0x10, 6) 159 | .storeAddress(this.address) 160 | .storeCoins(opts.value) 161 | .storeUint(0, 107) 162 | .storeSlice(body.asSlice()) 163 | .endCell(); 164 | */ 165 | } 166 | 167 | packActions(messages: OutAction[], value: bigint = toNano('1'), query_id: HighloadQueryId) { 168 | let batch: OutAction[]; 169 | if (messages.length > 254) { 170 | batch = messages.slice(0, 253); 171 | batch.push({ 172 | type: 'sendMsg', 173 | mode: value > 0n ? SendMode.PAY_GAS_SEPARATELY : SendMode.CARRY_ALL_REMAINING_BALANCE, 174 | outMsg: this.packActions(messages.slice(253), value, query_id) 175 | }); 176 | } else { 177 | batch = messages; 178 | } 179 | return this.createInternalTransfer({ 180 | actions: batch, 181 | queryId: query_id, 182 | value 183 | }); 184 | } 185 | 186 | 187 | async getPublicKey(provider: ContractProvider): Promise { 188 | const res = (await provider.get('get_public_key', [])).stack; 189 | const pubKeyU = res.readBigNumber(); 190 | return Buffer.from(pubKeyU.toString(16).padStart(32 * 2, '0'), 'hex'); 191 | } 192 | 193 | async getSubwalletId(provider: ContractProvider): Promise { 194 | const res = (await provider.get('get_subwallet_id', [])).stack; 195 | return res.readNumber(); 196 | } 197 | 198 | async getTimeout(provider: ContractProvider): Promise { 199 | const res = (await provider.get('get_timeout', [])).stack; 200 | return res.readNumber(); 201 | } 202 | 203 | async getLastCleaned(provider: ContractProvider): Promise { 204 | const res = (await provider.get('get_last_clean_time', [])).stack; 205 | return res.readNumber(); 206 | } 207 | 208 | async getProcessed(provider: ContractProvider, queryId: HighloadQueryId, needClean = true): Promise { 209 | const res = (await provider.get('processed?', [{'type': 'int', 'value': queryId.getQueryId()}, { 210 | 'type': 'int', 211 | 'value': needClean ? -1n : 0n 212 | }])).stack; 213 | return res.readBoolean(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /wrappers/MsgGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Cell, CommonMessageInfoExternalIn, CommonMessageInfoExternalOut, ExternalAddress, Message, MessageRelaxed, StateInit, beginCell, external, storeMessage, storeMessageRelaxed } from '@ton/core'; 2 | import { randomAddress } from '@ton/test-utils'; 3 | export class MsgGenerator { 4 | constructor(readonly wc: number){} 5 | 6 | generateExternalOutWithBadSource() { 7 | const ssrcInvalid = beginCell() 8 | .storeUint(2, 2) // addr_std$10 9 | .storeUint(0, 1) // anycast nothing 10 | .storeInt(this.wc, 8) // workchain_id: -1 11 | .storeUint(1, 10) 12 | .endCell() 13 | 14 | return beginCell() 15 | .storeUint(3, 2) // ext_out_msg_info$11 16 | .storeBit(0) // src:INVALID 17 | .storeSlice(ssrcInvalid.beginParse()) 18 | .endCell(); 19 | 20 | } 21 | generateExternalOutWithBadDst() { 22 | const src = randomAddress(-1); 23 | return beginCell() 24 | .storeUint(3, 2) // ext_out_msg_info$11 25 | .storeAddress(src) // src:MsgAddressInt 26 | .storeBit(0) // dest:INVALID 27 | .endCell(); 28 | } 29 | generateExternalInWithBadSource() { 30 | const ssrcInvalid = beginCell() 31 | .storeUint(1, 2) // addrExtern$01 32 | .storeUint(128, 9) 33 | .storeUint(0, 10) 34 | .endCell() 35 | 36 | return beginCell() 37 | .storeUint(2, 2) //ext_in_msg_info$11 38 | .storeSlice(ssrcInvalid.beginParse()) // src:INVALID 39 | .endCell(); 40 | } 41 | generateExternalInWithBadDst() { 42 | const src = new ExternalAddress(BigInt(Date.now()), 256); 43 | return beginCell() 44 | .storeUint(2, 2) //ext_in_msg_info$10 45 | .storeAddress(src) // src:MsgAddressExt 46 | .storeBit(0) // dest:INVALID 47 | .endCell(); 48 | } 49 | generateInternalMessageWithBadGrams() { 50 | const src = randomAddress(this.wc); 51 | const dst = randomAddress(this.wc); 52 | return beginCell() 53 | .storeUint(0, 1) // int_msg_info$0 54 | .storeUint(0, 1) // ihr_disabled:Bool 55 | .storeUint(0, 1) // bounce:Bool 56 | .storeUint(0, 1) // bounced:Bool 57 | .storeAddress(src) // src:MsgAddress 58 | .storeAddress(dst) // dest:MsgAddress 59 | .storeUint(8, 4) // len of nanograms 60 | .storeUint(1, 1) // INVALID GRAMS amount 61 | .endCell(); 62 | 63 | } 64 | generateInternalMessageWithBadInitStateData() { 65 | const ssrc = randomAddress(this.wc); 66 | const sdest = randomAddress(this.wc); 67 | 68 | const init_state_with_bad_data = beginCell().storeUint(0, 1) // maybe (##5) 69 | .storeUint(1, 1) // Maybe TickTock 70 | .storeUint(1, 1) // bool Tick 71 | .storeUint(0, 1) // bool Tock 72 | .storeUint(1, 1) // code: Maybe Cell^ 73 | .storeUint(1, 1) // data: Maybe Cell^ 74 | .storeUint(1, 1); // library: Maybe ^Cell 75 | // bits for references but no data 76 | 77 | return beginCell() 78 | .storeUint(0, 1) // int_msg_info$0 79 | .storeUint(0, 1) // ihr_disabled:Bool 80 | .storeUint(0, 1) // bounce:Bool 81 | .storeUint(0, 1) // bounced:Bool 82 | .storeAddress(ssrc) // src:MsgAddress 83 | .storeAddress(sdest) // dest:MsgAddress 84 | .storeCoins(0) // 85 | .storeMaybeRef(null) // extra currencies 86 | .storeCoins(0) // ihr_fee 87 | .storeCoins(0) // fwd_fee 88 | .storeUint(1000, 64) // created_lt:uint64 89 | .storeUint(1000, 32) // created_at:uint32 90 | .storeUint(1, 1) // Maybe init_state 91 | .storeUint(1, 1) // Either (X ^X) init state 92 | .storeRef(init_state_with_bad_data.endCell()) 93 | .storeUint(0, 1) // Either (X ^X) body 94 | .endCell(); 95 | } 96 | 97 | *generateBadMsg() { 98 | // Meh 99 | yield this.generateExternalInWithBadDst(); 100 | yield this.generateExternalOutWithBadDst(); 101 | yield this.generateExternalInWithBadSource(); 102 | yield this.generateExternalOutWithBadSource(); 103 | yield this.generateInternalMessageWithBadGrams(); 104 | yield this.generateInternalMessageWithBadInitStateData(); 105 | } 106 | generateExternalInMsg(info?: Partial, body?: Cell, init?: StateInit) { 107 | const msgInfo: CommonMessageInfoExternalIn = { 108 | type: 'external-in', 109 | dest: info?.dest || randomAddress(this.wc), 110 | src: info?.src, 111 | importFee: info?.importFee || 0n 112 | } 113 | const newMsg: Message = { 114 | info: msgInfo, 115 | body: body || Cell.EMPTY, 116 | init 117 | } 118 | return beginCell().store(storeMessage(newMsg)).endCell(); 119 | } 120 | generateExternalOutMsg(info?: Partial, body?: Cell) { 121 | const msgInfo: CommonMessageInfoExternalOut = { 122 | type: 'external-out', 123 | createdAt: info?.createdAt || 0, 124 | createdLt: info?.createdLt || 0n, 125 | src: info?.src || randomAddress(this.wc), 126 | dest: info?.dest 127 | } 128 | const newMsg: MessageRelaxed = { 129 | info: msgInfo, 130 | body: body || Cell.EMPTY, 131 | } 132 | return beginCell().store(storeMessageRelaxed(newMsg)).endCell(); 133 | } 134 | } 135 | --------------------------------------------------------------------------------