diff --git a/docs.json b/docs.json index d89495e5..1cf65340 100644 --- a/docs.json +++ b/docs.json @@ -288,8 +288,14 @@ "standard/wallets/mnemonics", "standard/wallets/comparison", "standard/wallets/history", - "standard/wallets/v5", "standard/wallets/v4", + { + "group": "V5", + "pages": [ + "standard/wallets/v5", + "standard/wallets/v5-api" + ] + }, "standard/wallets/highload", "standard/wallets/multisig" ] diff --git a/standard/wallets/history.mdx b/standard/wallets/history.mdx index 4cbeae29..5d78cab3 100644 --- a/standard/wallets/history.mdx +++ b/standard/wallets/history.mdx @@ -29,9 +29,8 @@ You can consider each wallet version as a smart contract implementation providin Here, you can find the current hashes of the wallet contract code versions. For detailed specifications of each wallet contract, please refer further down the page or check the [ContractSources.md](https://github.com/toncenter/tonweb/blob/update_contracts/src/contract/ContractSources.md). - | Contract version | Hash | -|---------------------|--------------------------------------------------------------------| +| ------------------- | ------------------------------------------------------------------ | | [V1 R1](#wallet-v1) | `a0cfc2c48aee16a271f2cfc0b7382d81756cecb1017d077faaab3bb602f6868c` | | [V1 R2](#wallet-v1) | `d4902fcc9fad74698fa8e353220a68da0dcf72e32bcb2eb9ee04217c17d3062c` | | [V1 R3](#wallet-v1) | `587cc789eff1c84f46ec3797e45fc809a14ff5ae24f1e0c7a6a99cc9dc9061ff` | @@ -43,7 +42,6 @@ For detailed specifications of each wallet contract, please refer further down t | [V4 R2](#wallet-v4) | `feb5ff6820e2ff0d9483e7e0d62c817d846789fb4ae580c878866d959dabd5c0` | | [V5 R1](#wallet-v5) | `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f` | - **Note:** These hashes can also be found in the explorers. ### Wallet V1 @@ -66,12 +64,11 @@ Nevertheless, because each subsequent version inherits the functionality of the #### Official code hashes | Contract version | Hash | -|------------------|--------------------------------------------------------------------| +| ---------------- | ------------------------------------------------------------------ | | V1 R1 | `a0cfc2c48aee16a271f2cfc0b7382d81756cecb1017d077faaab3bb602f6868c` | | V1 R2 | `d4902fcc9fad74698fa8e353220a68da0dcf72e32bcb2eb9ee04217c17d3062c` | | V1 R3 | `587cc789eff1c84f46ec3797e45fc809a14ff5ae24f1e0c7a6a99cc9dc9061ff` | - #### Persistent memory layout - seqno: 32-bit long sequence number. @@ -83,12 +80,12 @@ Nevertheless, because each subsequent version inherits the functionality of the - signature: 512-bit long Ed25519 signature. - msg-seqno: 32-bit long sequence number. - (0-4) mode: up to four 8-bit long integers defining sending mode for each message. -2. Up to 4 references to cells containing messages. +1. Up to 4 references to cells containing messages. As you can see, the main functionality of the wallet is to provide a safe way to communicate with TON Blockchain from the outside world. The `seqno` mechanism protects against replay attacks, and the `Ed25519 signature` provides authorized access to wallet functionality. The payload data consists of up to 4 references to cells and the corresponding number of modes, which will be directly transferred to the `send_raw_message` method. #### Exit codes @@ -99,12 +96,10 @@ Note that the wallet doesn't provide any validation for internal messages you se | 34 | `Ed25519 signature` check failed | | 0 | Standard successful execution exit code. | - #### Get methods 1. `int seqno()` returns current stored seqno. -2. `int get_public_key()` returns current stored public key. - +1. `int get_public_key()` returns current stored public key. ### Wallet V2 @@ -113,8 +108,8 @@ Wallet source code: - [ton/crypto/smartcont/wallet-code.fc](https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/wallet-code.fc) This version introduces the `valid_until` parameter, which is used to set a time limit for a transaction in case you don't want it to be confirmed too late. This version also does not have a get method for the public key, which was added in `V2R2`. @@ -124,7 +119,7 @@ All differences compared to the previous version are a consequence of adding the #### Official code hashes | Contract version | Hash | -|------------------|--------------------------------------------------------------------| +| ---------------- | ------------------------------------------------------------------ | | V2 R1 | `5c9a5e68c108e18721a07c42f9956bfb39ad77ec6d624b60c576ec88eee65329` | | V2 R2 | `fe9530d3243853083ef2ef0b4c2908c0abf6fa1c31ea243aacaa5bf8c7d753f1` | @@ -135,8 +130,7 @@ All differences compared to the previous version are a consequence of adding the - msg-seqno: 32-bit long sequence number. - valid-until: 32-bit long Unix time integer. - (0-4) mode: up to four 8-bit integers defining the sending mode for each message. -2. Up to 4 references to cells containing messages. - +1. Up to 4 references to cells containing messages. ### Wallet V3 @@ -147,41 +141,39 @@ Wallet source code: - [ton/crypto/smartcont/wallet3-code.fc](https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/wallet3-code.fc) Essentially, `subwallet_id` is just a number added to the contract state when it’s deployed. Since the contract address in TON is a hash of its state and code, the wallet address will change with a different `subwallet_id`. This version is the most widely used right now. It covers most use cases and remains clean, simple, and mostly the same as previous versions. All get methods remain the same. - #### Official code hashes | Contract version | Hash | -|------------------|--------------------------------------------------------------------| +| ---------------- | ------------------------------------------------------------------ | | V3 R1 | `b61041a58a7980b946e8fb9e198e3c904d24799ffa36574ea4251c41a566f581` | | V3 R2 | `84dafa449f98a6987789ba232358072bc0f76dc4524002a5d0918b9a75d2d599` | - #### Persistent memory layout - seqno: 32-bit sequence number. -- subwallet_id: 32-bit subwallet ID. +- subwallet\_id: 32-bit subwallet ID. - public-key: 256-bit public key. #### External message body layout 1. Data: - signature: 512-bit Ed25519 signature. - - subwallet_id: 32-bit subwallet ID. + - subwallet\_id: 32-bit subwallet ID. - msg-seqno: 32-bit sequence number. - valid-until: 32-bit Unix time integer. - (0-4) mode: up to four 8-bit integers defining the sending mode for each message. -2. Up to 4 references to cells containing messages. +1. Up to 4 references to cells containing messages. #### Exit codes | Exit code | Description | -|-----------|-------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------- | | 33 | `seqno` check failed; replay protection triggered | | 34 | `subwallet_id` does not match the stored one | | 35 | `valid_until` check failed; transaction confirmation attempted too late | @@ -189,235 +181,21 @@ Essentially, `subwallet_id` is just a number added to the contract state when it | 0 | Standard successful execution exit code. | ### Wallet V4 -This version retains all the functionality of the previous versions but also introduces something very powerful: `plugins`. - -Wallet source code: - -- [ton-blockchain/wallet-contract](https://github.com/ton-blockchain/wallet-contract) - -This feature allows developers to implement complex logic that works in tandem with a user's wallet. For example, a DApp may require a user to pay a small amount of coins every day to use certain features. In this case, the user would need to install the plugin on their wallet by signing a transaction. The plugin would then send coins to the destination address daily when requested by an external message. - -#### Official code hashes - -| Contract version | Hash | -|------------------|--------------------------------------------------------------------| -| V4 R1 | `64dd54805522c5be8a9db59cea0105ccf0d08786ca79beb8cb79e880a8d7322d` | -| V4 R2 | `feb5ff6820e2ff0d9483e7e0d62c817d846789fb4ae580c878866d959dabd5c0` | - -#### Plugins - -Plugins are essentially other smart contracts on TON that developers are free to implement as they wish. In relation to the wallet, they are simply addresses of smart contracts stored in a dictionary in the wallet's persistent memory. These plugins are allowed to request funds and remove themselves from the "allowed list" by sending internal messages to the wallet. - -#### Persistent memory layout - -- seqno: 32-bit long sequence number. -- subwallet_id: 32-bit long subwallet_id. -- public-key: 256-bit long public key. -- plugins: dictionary containing plugins (may be empty) - -#### Receiving internal messages - -All previous versions of wallets had a straightforward implementation for receiving internal messages. They simply accepted incoming funds from any sender, ignoring the internal message body if present, or in other words, they had an empty `recv_internal` method. However, as mentioned earlier, the fourth version of the wallet introduces two additional available operations. Let's take a look at the internal message body layout: - -- opcode?: 32-bit long operation code. This is an optional field. Any message containing less than 32 bits in the message body, an incorrect opcode, or a sender address that isn't registered as a plugin will be considered a simple transfer, similar to previous wallet versions. -- query-id: 64-bit long integer. This field has no effect on the smart contract's behavior; it is used to track chains of messages between contracts. - -1. opcode = 0x706c7567, request funds operation code. - - Toncoin: VARUINT16 amount of requested Toncoin. - - extra_currencies: dictionary containing the amount of requested extra currencies (may be empty). -2. opcode = 0x64737472, request removal of plugin-sender from the "allowed list". - -#### External message body layout - -- signature: 512-bit long Ed25519 signature. -- subwallet_id: 32-bit long subwallet ID. -- valid-until: 32-bit long Unix time integer. -- msg-seqno: 32-bit long sequence number. -- opcode: 32-bit long operation code. - -1. opcode = 0x0, simple send. - - (0-4) mode: up to four 8-bit integers defining the sending mode for each message. - - (0-4) messages: up to four references to cells containing messages. -2. opcode = 0x1, deploy and install plugin. - - workchain: 8-bit long integer. - - balance: VARUINT16 Toncoin amount of initial balance. - - state_init: cell reference containing plugin initial state. - - body: cell reference containing body. -3. opcode = 0x2/0x3, install plugin/remove plugin. - - wc_n_address: 8-bit long compact workchain ID specific to this layout + 256-bit long plugin address. - - balance: VARUINT16 Toncoin amount of initial balance. - - query-id: 64-bit long integer. - -As you can see, the fourth version still provides standard functionality through the `0x0` opcode, similar to previous versions. The `0x2` and `0x3` operations allow manipulation of the plugin dictionary. Note that in the case of `0x2`, you need to deploy the plugin with that address yourself. In contrast, the `0x1` opcode also handles the deployment process with the `state_init` field. - -#### Exit codes - -| Exit code | Description | -|-----------| ----------------------------------------------------------------------- | -| 33 | `seqno` check failed, replay protection triggered | -| 34 | `subwallet_id` does not match the stored one | -| 35 | `Ed25519 signature` check failed | -| 36 | `valid_until` check failed, transaction confirmation attempted too late | -| 39 | Plugins dictionary manipulation failed (0x1-0x3 recv_external opcodes) | -| 80 | Not enough funds for the funds request | -| 0 | Standard successful execution exit code. | - -#### Get methods - -1. `int seqno()` returns current stored seqno. -2. `int get_public_key()` returns current stored public key. -3. `int get_subwallet_id()` returns current subwallet ID. -4. `int is_plugin_installed(int wc, int addr_hash)` checks if plugin with defined workchain_id and address hash is installed. -5. `tuple get_plugin_list()` returns list of plugins. - +Read about this standard iteration on its dedicated page: [V4 wallet standard](/standard/wallets/v4). ### Wallet V5 It is the most modern wallet version at the moment, developed by the Tonkeeper team, aimed at replacing V4 and allowing arbitrary extensions. -![v5](/resources/images/wallets/wallet-contract-V5.png) - - -The V5 wallet standard offers many benefits that improve the experience for both users and merchants. V5 supports gasless transactions, account delegation and recovery, subscription payments using tokens and Toncoin, and low-cost multi-transfers. In addition to retaining the previous functionality (V4), the new contract allows you to send up to 255 messages at a time. - -Wallet source code: - -- [ton-blockchain/wallet-contract-v5](https://github.com/ton-blockchain/wallet-contract-v5) - -TL-B scheme: - -- [ton-blockchain/wallet-contract-v5/types.tlb](https://github.com/ton-blockchain/wallet-contract-v5/blob/main/types.tlb) - - -#### Official code hash - -| Contract version | Hash | -|------------------|--------------------------------------------------------------------| -| V5 R1 | `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f` | - - -#### Persistent memory layout - -```tlb -contract_state$_ - is_signature_allowed:(## 1) - seqno:# - wallet_id:(## 32) - public_key:(## 256) - extensions_dict:(HashmapE 256 int1) = ContractState; -``` - -As you can see, the `ContractState`, compared to previous versions, hasn't undergone major changes. The main difference is the new `is_signature_allowed` 1-bit flag, which restricts or allows access through the signature and stored public key. We will describe the importance of this change in later topics. - -#### Authentication process - -```tlb -signed_request$_ // 32 (opcode from outer) - wallet_id: # // 32 - valid_until: # // 32 - msg_seqno: # // 32 - inner: InnerRequest // - signature: bits512 // 512 -= SignedRequest; // Total: 688 .. 976 + ^Cell - -internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; - -internal_extension#6578746e - query_id:(## 64) - inner:InnerRequest = InternalMsgBody; - -external_signed#7369676e signed:SignedRequest = ExternalMsgBody; -``` - -Before we get to the actual payload of our messages — `InnerRequest` — let's first look at how version 5 differs from previous versions in the authentication process. The `InternalMsgBody` combinator describes two ways to access wallet actions through internal messages. The first method is one we are already familiar with from version 4: authentication as a previously registered extension, the address of which is stored in `extensions_dict`. The second method is authentication through the stored public key and signature, similar to external requests. - -At first, this might seem like an unnecessary feature, but it actually enables requests to be processed through external services (smart contracts) that are not part of your wallet's extension infrastructure — a key feature of V5. Gasless transactions rely on this functionality. - -Note that simply receiving funds is still an option. Practically, any received internal message that doesn't pass the authentication process will be considered a transfer. - -#### Actions - -The first thing that we should notice is `InnerRequest`, which we have already seen in the authentication process. In contrast to the previous version, both external and internal messages have access to the same functionality, except for changing the signature mode (i.e., the `is_signature_allowed` flag). - -```tlb -out_list_empty$_ = OutList 0; -out_list$_ {n:#} - prev:^(OutList n) - action:OutAction = OutList (n + 1); - -action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; - -// Extended actions in V5: -action_list_basic$_ {n:#} actions:^(OutList n) = ActionList n 0; -action_list_extended$_ {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); - -action_add_ext#02 addr:MsgAddressInt = ExtendedAction; -action_delete_ext#03 addr:MsgAddressInt = ExtendedAction; -action_set_signature_auth_allowed#04 allowed:(## 1) = ExtendedAction; - -actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; -``` - -We can consider `InnerRequest` as two lists of actions: the first, `OutList`, is an optional chain of cell references, each containing a send message request led by the message mode. The second, `ActionList,` is led by a one-bit flag, `has_other_actions`, which marks the presence of extended actions, starting from the first cell and continuing as a chain of cell references. We are already familiar with the first two extended actions, `action_add_ext` and `action_delete_ext`, followed by the internal address that we want to add or delete from the extensions dictionary. The third, `action_set_signature_auth_allowed`, restricts or allows authentication through the public key, leaving the only way to interact with the wallet through extensions. This functionality might be extremely important in the case of a lost or compromised private key. - -#### Exit codes - -| Exit code | Description | -|-----------| --------------------------------------------------------------------------------- | -| 132 | Authentication attempt through signature while it's disabled | -| 133 | `seqno` check failed, replay protection occurred | -| 134 | `wallet_id` does not correspond to the stored one | -| 135 | `Ed25519 signature` check failed | -| 136 | `valid-until` check failed | -| 137 | Enforce that `send_mode` has the +2 bit (ignore errors) set for external messages. | -| 138 | `external_signed` prefix doesn't correspond to the received one | -| 139 | Add extension operation was not successful | -| 140 | Remove extension operation was not successful | -| 141 | Unsupported extended message prefix | -| 142 | Tried to disable auth by signature while the extension dictionary is empty | -| 143 | Attempt to set signature to an already set state | -| 144 | Tried to remove the last extension when signature is disabled | -| 145 | Extension has the wrong workchain | -| 146 | Tried to change signature mode through external message | -| 147 | Invalid `c5`, `action_send_msg` verification failed | -| 0 | Standard successful execution exit code. | - - - -#### Get methods - -1. `int is_signature_allowed()` returns stored `is_signature_allowed` flag. -2. `int seqno()` returns current stored seqno. -3. `int get_wallet_id()` returns current wallet ID. -4. `int get_public_key()` returns current stored public key. -5. `cell get_extensions()` returns extensions dictionary. - -#### Preparing for gasless transactions - -Starting with v5, the wallet smart contract supports owner-signed internal messages (`internal_signed`), which enables gasless transactions—for example, paying network fees in USDt when transferring USDt. The common scheme looks like this: - -![Gasless transaction flow diagram](/resources/images/wallets/gasless.jpg) - -#### Flow - -1. When sending USDt, the user signs one message containing two outgoing USDt transfers: - 1. USDt transfer to the recipient's address. - 2. Transfer of a small amount of USDt in favor of the service. -2. This signed message is sent off-chain by HTTPS to the service backend. The service backend sends it to the TON Blockchain, paying Toncoin for network fees. - -The beta version of the gasless backend API is available at [tonapi.io/api-v2](https://tonapi.io/api-v2). If you are developing a wallet app and have feedback about these methods, please share it in [@tonapitech](https://t.me/tonapitech) chat. - -Wallet source code: - -- [ton-blockchain/wallet-contract-v5](https://github.com/ton-blockchain/wallet-contract-v5) +Read more: +- [V5 wallet standard](/standard/wallets/v5) +- [How to interact with V5 wallet](/standard/wallets/v5-api) ## Special wallets diff --git a/standard/wallets/v4.mdx b/standard/wallets/v4.mdx index e69de29b..940f7e0e 100644 --- a/standard/wallets/v4.mdx +++ b/standard/wallets/v4.mdx @@ -0,0 +1,87 @@ +--- +title: "Wallet standard V4" +sidebarTitle: "V4" +--- + +This article provides overview of V4 wallet standard. + +This version retains all the functionality of the previous versions but also introduces something very powerful: `plugins`. + +Wallet source code: + +- [ton-blockchain/wallet-contract](https://github.com/ton-blockchain/wallet-contract) + +This feature allows developers to implement complex logic that works in tandem with a user's wallet. For example, a DApp may require a user to pay a small amount of coins every day to use certain features. In this case, the user would need to install the plugin on their wallet by signing a transaction. The plugin would then send coins to the destination address daily when requested by an external message. + +#### Official code hashes + +| Contract version | Hash | +| ---------------- | ------------------------------------------------------------------ | +| V4 R1 | `64dd54805522c5be8a9db59cea0105ccf0d08786ca79beb8cb79e880a8d7322d` | +| V4 R2 | `feb5ff6820e2ff0d9483e7e0d62c817d846789fb4ae580c878866d959dabd5c0` | + +#### Plugins + +Plugins are essentially other smart contracts on TON that developers are free to implement as they wish. In relation to the wallet, they are simply addresses of smart contracts stored in a dictionary in the wallet's persistent memory. These plugins are allowed to request funds and remove themselves from the "allowed list" by sending internal messages to the wallet. + +#### Persistent memory layout + +- seqno: 32-bit long sequence number. +- subwallet\_id: 32-bit long subwallet\_id. +- public-key: 256-bit long public key. +- plugins: dictionary containing plugins (may be empty) + +#### Receiving internal messages + +All previous versions of wallets had a straightforward implementation for receiving internal messages. They simply accepted incoming funds from any sender, ignoring the internal message body if present, or in other words, they had an empty `recv_internal` method. However, as mentioned earlier, the fourth version of the wallet introduces two additional available operations. Let's take a look at the internal message body layout: + +- opcode?: 32-bit long operation code. This is an optional field. Any message containing less than 32 bits in the message body, an incorrect opcode, or a sender address that isn't registered as a plugin will be considered a simple transfer, similar to previous wallet versions. +- query-id: 64-bit long integer. This field has no effect on the smart contract's behavior; it is used to track chains of messages between contracts. + +1. opcode = 0x706c7567, request funds operation code. + - Toncoin: VARUINT16 amount of requested Toncoin. + - extra\_currencies: dictionary containing the amount of requested extra currencies (may be empty). +1. opcode = 0x64737472, request removal of plugin-sender from the "allowed list". + +#### External message body layout + +- signature: 512-bit long Ed25519 signature. +- subwallet\_id: 32-bit long subwallet ID. +- valid-until: 32-bit long Unix time integer. +- msg-seqno: 32-bit long sequence number. +- opcode: 32-bit long operation code. + +1. opcode = 0x0, simple send. + - (0-4) mode: up to four 8-bit integers defining the sending mode for each message. + - (0-4) messages: up to four references to cells containing messages. +1. opcode = 0x1, deploy and install plugin. + - workchain: 8-bit long integer. + - balance: VARUINT16 Toncoin amount of initial balance. + - state\_init: cell reference containing plugin initial state. + - body: cell reference containing body. +1. opcode = 0x2/0x3, install plugin/remove plugin. + - wc\_n\_address: 8-bit long compact workchain ID specific to this layout + 256-bit long plugin address. + - balance: VARUINT16 Toncoin amount of initial balance. + - query-id: 64-bit long integer. + +As you can see, the fourth version still provides standard functionality through the `0x0` opcode, similar to previous versions. The `0x2` and `0x3` operations allow manipulation of the plugin dictionary. Note that in the case of `0x2`, you need to deploy the plugin with that address yourself. In contrast, the `0x1` opcode also handles the deployment process with the `state_init` field. + +#### Exit codes + +| Exit code | Description | +| --------- | ----------------------------------------------------------------------- | +| 33 | `seqno` check failed, replay protection triggered | +| 34 | `subwallet_id` does not match the stored one | +| 35 | `Ed25519 signature` check failed | +| 36 | `valid_until` check failed, transaction confirmation attempted too late | +| 39 | Plugins dictionary manipulation failed (0x1-0x3 recv\_external opcodes) | +| 80 | Not enough funds for the funds request | +| 0 | Standard successful execution exit code. | + +#### Get methods + +1. `int seqno()` returns current stored seqno. +1. `int get_public_key()` returns current stored public key. +1. `int get_subwallet_id()` returns current subwallet ID. +1. `int is_plugin_installed(int wc, int addr_hash)` checks if plugin with defined workchain\_id and address hash is installed. +1. `tuple get_plugin_list()` returns list of plugins. diff --git a/standard/wallets/v5-api.mdx b/standard/wallets/v5-api.mdx new file mode 100644 index 00000000..acaef51e --- /dev/null +++ b/standard/wallets/v5-api.mdx @@ -0,0 +1,522 @@ +--- +title: "Wallet V5 API" +sidebarTitle: "How to interact" +--- + +import { Aside } from "/snippets/aside.jsx"; + +This article provides overview of wallet V5 public interfaces, how to interact with them and serialize used data structures. + + + +There are several ways how you can interact with deployed V5 wallet smart contract: + +- Send external signed message +- Send internal signed message +- Send internal message from extension + +Let's first explore message structure, that is used to perform different actions on wallet contract. + +## Message structure + +Message structure for V5 wallet contract is quite cumbersome and hard to read, it's made for optimal (de-)serialization and not optimized for understanding. It is described in [TL-B](/language/TL-B/overview) language and includes snake-cell pattern. We will try to get a grip of it by breaking down core data structures and how they are used. + +You can skip to [Examples section](#examples), where we would use existing high-level libraries that abstract low level logic from the user. + +### TL-B + +This is TL-B for V5 wallet actions, it includes some [complex TL-B patterns](/language/TL-B/complex-and-non-trivial-examples.mdx). You can also find it on [GitHib](https://github.com/ton-blockchain/wallet-contract-v5/blob/321186127e8cc5e395ad3b2f1870839237c56f5f/types.tlb), in the wallet repo. + +```tlb expandable +// Standard actions from block.tlb: +out_list_empty$_ = OutList 0; +out_list$_ {n:#} prev:^(OutList n) action:OutAction = OutList (n + 1); +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; + +// Extended actions in W5: +action_list_basic$_ {n:#} actions:^(OutList n) = ActionList n 0; +action_list_extended$_ {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); + +action_add_ext#02 addr:MsgAddressInt = ExtendedAction; +action_delete_ext#03 addr:MsgAddressInt = ExtendedAction; +action_set_signature_auth_allowed#04 allowed:(## 1) = ExtendedAction; + +signed_request$_ // 32 (opcode from outer) + wallet_id: # // 32 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell + +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; +internal_extension#6578746e query_id:(## 64) inner:InnerRequest = InternalMsgBody; +external_signed#7369676e signed:SignedRequest = ExternalMsgBody; + +actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; + +// Contract state +contract_state$_ is_signature_allowed:(## 1) seqno:# wallet_id:(## 32) public_key:(## 256) extensions_dict:(HashmapE 256 int1) = ContractState; +``` + +Three types of messages that were described above can be seen here: + +```tlb +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; +internal_extension#6578746e query_id:(## 64) inner:InnerRequest = InternalMsgBody; +external_signed#7369676e signed:SignedRequest = ExternalMsgBody; +``` + +Each of them includes the same `InnerRequest` field that dictates whats need to be done by wallet contract. In case of signed messages, the request needs to be verified, so `InnerRequest` is wrapped in `SignedRequest` structure, which contains necessary information for this. + +Let's break down these data structures. + +### Signed Request + +Signed message is a message that was signed using owners private key from his dedicated keypair, method from asymmetric cryptography. Later this message will be verified on-chain using public key stored in wallet smart contract - [read more](/standard/wallets/how-it-works#how-ownership-verification-works) about how ownership verification works. + +Before V5 standard, there was only one way to deliver signed message to wallet contract - via external-in message. However, external messages has certain limitations, e.g. you can only send external-out messages from the smart contracts themselves. This means that it wasn't possible to deliver signed message from inside the blockchain, from another smart contract. V5 standard adds this functionality, partially enabling [gassless transaction](/standard/wallets/v5#preparing-for-gasless-transactions). + +Besides `InnerRequest` field that contains actual actions that will be performed, `Signed message` structure contains usual wallet message fields that were in-place in previous versions, read more about them [here](/standard/wallets/how-it-works). + +### Inner Request + +Inner request is defined as follows: + +```tlb +actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; +``` + +V5 wallet supports two main types of actions that can be performed: + +```mermaid +graph TD + A[Root Actions] --> B[Send Message Action] + A --> C[Extended Actions] + C --> D[Add Extension] + C --> E[Delete Extension] + C --> F[Set Signature Auth] +``` + +The action structure allows for: +- **Send Message Actions**: Standard message sending with specified mode +- **Extended Actions**: Advanced wallet management operations + - **Add Extension**: Register new extension addresses + - **Delete Extension**: Remove extension addresses + - **Set Signature Auth**: Enable/disable signature-based authentication + +As you can see in TL-B, `out_actions` are snake-cell list of ordinary out messages, followed then by binary flag `has_other_actions` and `other_actions` extended action list. + +#### Inner Request Structure + +The Inner Request serialization follows this structure: + +```mermaid +graph TD + A[Inner Request] --> B[out_actions: Maybe OutList] + A --> C[has_other_actions: 1 bit] + + B --> B1{Has out actions?} + B1 -->|Yes| B2[OutList Chain] + B1 -->|No| B3[Empty/Nothing] + + B2 --> B4[Mode: 8 bits] + B2 --> B5[Message Reference] + B2 --> B6[Previous OutList Reference] + + C --> C1{Bit = 1?} + C1 -->|Yes| D[other_actions: ActionList] + C1 -->|No| D1[No extended actions] + + D --> D2[Extended Action] + D --> D3[Previous ActionList Reference] + + D2 --> D4[Add Extension #02] + D2 --> D5[Delete Extension #03] + D2 --> D6[Set Signature Auth #04] + + style A fill:#e1f5fe + style B2 fill:#f3e5f5 + style D fill:#e8f5e8 +``` + +#### Serialization Layout + +The Inner Request is serialized in the following order: + +```mermaid +graph LR + subgraph "Cell Data" + A[out_actions flag] --> B[has_other_actions: 1 bit] + end + + subgraph "References" + C[Ref 0: OutList Chain] + D[Ref 1: ActionList Chain] + end + + A -.-> C + B -.-> D + + subgraph "OutList Structure" + E[Mode: 8 bits] --> F[Message Ref] --> G[Prev OutList Ref] + end + + subgraph "ActionList Structure" + H[Action Type: 8 bits] --> I[Action Data] --> J[Prev ActionList Ref] + end + + C --> E + D --> H +``` + + +## Examples + +Here we will take a look at code examples in Typescript using low level serialization library *\@ton/core*. + +### How to create Inner Request + +As per message structure section above, `Inner Request` consists of 2 kinds of actions, basic *send message* actions and *extended actions* that affect contract behavior. + +Let's write code that handles packing for extended actions: + +```ts expandable +import { + Address, + beginCell, + Builder, + Cell +} from '@ton/core'; + +// declare actions as tagged union +export type OutActionAddExtension = { + type: 'addExtension'; + address: Address; +} + +export type OutActionRemoveExtension = { + type: 'removeExtension'; + address: Address; +} + +export type OutActionSetIsPublicKeyEnabled = { + type: 'setIsPublicKeyEnabled'; + isEnabled: boolean; +} + +export type OutActionExtended = OutActionSetIsPublicKeyEnabled | OutActionAddExtension | OutActionRemoveExtension; + +// store each action content as described in its TL-B specification: +// 8 bits for action tag and then useful payload + +const outActionAddExtensionTag = 0x02; +function storeOutActionAddExtension(action: OutActionAddExtension) { + return (builder: Builder) => { + builder.storeUint(outActionAddExtensionTag, 8).storeAddress(action.address) + } +} + +const outActionRemoveExtensionTag = 0x03; +function storeOutActionRemoveExtension(action: OutActionRemoveExtension) { + return (builder: Builder) => { + builder.storeUint(outActionRemoveExtensionTag, 8).storeAddress(action.address) + } +} + +const outActionSetIsPublicKeyEnabledTag = 0x04; +function storeOutActionSetIsPublicKeyEnabled(action: OutActionSetIsPublicKeyEnabled) { + return (builder: Builder) => { + builder.storeUint(outActionSetIsPublicKeyEnabledTag, 8).storeUint(action.isEnabled ? 1 : 0, 1) + } +} + +// entry point for storing any extended action +export function storeOutActionExtendedV5R1(action: OutActionExtended) { + switch (action.type) { + case 'setIsPublicKeyEnabled': + return storeOutActionSetIsPublicKeyEnabled(action); + case 'addExtension': + return storeOutActionAddExtension(action); + case 'removeExtension': + return storeOutActionRemoveExtension(action); + default: + throw new Error('Unknown action type' + (action as OutActionExtended)?.type); + } +} + +// and now the hard part - list snake-cell serialization; +// we will use this function recursively, to store actions as reference cells one by one +function packExtendedActionsRec(extendedActions: OutActionExtended[]): Cell { + const [first, ...rest] = extendedActions; + + let builder = beginCell() + .store(storeOutActionExtendedV5R1(first)); + + if (rest.length > 0) { + // if there are more actions, store them recursively + builder = builder.storeRef(packExtendedActionsRec(rest)); + } + + return builder.endCell(); +} +``` + +Now we have to deal with basic action serialization. However, since these are the messages that are described in [block.tlb](/language/TL-B/overview), we can use contract-agnostic code from serialization library to store them. + +Here is an code snippet for storing V5 wallet actions as per `Inner Request` TL-B: + +```ts expandable +import { + beginCell, + Builder, Cell, + loadOutList, + OutActionSendMsg, SendMode, + Slice, + storeOutList +} from '@ton/core'; + +// helper functions +export function isOutActionExtended(action: OutActionSendMsg | OutActionExtended): action is OutActionExtended { + return ( + action.type === 'setIsPublicKeyEnabled' || action.type === 'addExtension' || action.type === 'removeExtension' + ); +} + +export function isOutActionBasic(action: OutActionSendMsg | OutActionExtended): action is OutActionSendMsg { + return !isOutActionExtended(action); + +} + +// main entrypoint for storing any actions list +export function storeOutListExtendedV5(actions: (OutActionExtended | OutActionSendMsg)[]) { + const extendedActions = actions.filter(isOutActionExtended); + const basicActions = actions.filter(isOutActionBasic); + + return (builder: Builder) => { + // here we use "storeOutList", serialization function from @ton/core + // we reverse the list since cells are stored recursively via snake-cell, so we need the reverse order + const outListPacked = basicActions.length ? beginCell().store(storeOutList(basicActions.slice().reverse())) : null; + builder.storeMaybeRef(outListPacked); + + if (extendedActions.length === 0) { + // has_more_actions flag to false + builder.storeUint(0, 1); + } else { + const [first, ...rest] = extendedActions; + + builder + // has_more_actions flag to true + .storeUint(1, 1) + // here we use our store function from previous code section + .store(storeOutActionExtendedV5(first)); + // if there are more actions - store them one by one + if (rest.length > 0) { + builder.storeRef(packExtendedActionsRec(rest)); + } + } + } +} +``` + +With this, we can serialize and store list of actions, in next section we will learn how to use them to send signed message. + +### How to create and send Signed Request + +There are several additional arguments that we need to create signed message besides the list of actions from previous section: + +- `walletId` +- `seqno` +- `privateKey` +- `validUntil` + +You can read more about where to obtain them and what there purpose is in [How it works](/standard/wallets/how-it-works) section. + +In this code snippet we will learn how to create signed serialized message. + +```ts expandable +import { sign } from "@ton/crypto"; + +export type WalletV5BasicSendArgs = { + seqno: number; + validUntil: number; + privateKey: Buffer; +} + +// internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; +// external_signed#7369676e signed:SignedRequest = ExternalMsgBody; +export type WalletV5SendArgsSinged = WalletV5BasicSendArgs + & { authType?: 'external' | 'internal';}; + +// internal_extension#6578746e query_id:(## 64) inner:InnerRequest = InternalMsgBody; +export type Wallet5VSendArgsExtensionAuth = WalletV5BasicSendArgs & { + authType: 'extension'; + queryId?: bigint; +} + +export type WalletV5SendArgs = + | WalletV5SendArgsSinged + | Wallet5VSendArgsExtensionAuth; + +const OpCodes = { + authExtension: 0x6578746e, + authSignedExternal: 0x7369676e, + authSignedInternal: 0x73696e74 +} + +export function createWalletTransferV5R1( + args: T extends Wallet5VSendArgsExtensionAuth + // action types are from Inner Request section + ? T & { actions: (OutActionSendMsg | OutActionExtended)[]} + : T & { actions: (OutActionSendMsg | OutActionExtended)[], walletId: (builder: Builder) => void } +): Cell { + // Check number of actions + if (args.actions.length > 255) { + throw Error("Maximum number of OutActions in a single request is 255"); + } + + // store each message type according to its TL-B + + // internal_extension#6578746e query_id:(## 64) inner:InnerRequest = InternalMsgBody; + if (args.authType === 'extension') { + return beginCell() + .storeUint(OpCodes.authExtension, 32) + .storeUint(args.queryId ?? 0, 64) + // use storeOutListExtendedV5 + .store(storeOutListExtendedV5(args.actions)) + .endCell(); + } + + // internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; + // external_signed#7369676e signed:SignedRequest = ExternalMsgBody; + const signingMessage = beginCell() + .storeUint(args.authType === 'internal' + ? OpCodes.authSignedInternal + : OpCodes.authSignedExternal, 32) + .store(args.walletId); + + + // now we store common part for both messages + signingMessage.storeUint(args.timeout || Math.floor(Date.now() / 1e3) + 60, 32); // Default timeout: 60 seconds + + signingMessage + .storeUint(args.seqno, 32) + .store(storeOutListExtendedV5(args.actions)); + + // now we need to sign message + const signature = sign(signingMessage.endCell().hash(), args.secretKey); + + return beginCell().storeBuilder(signingMessage).storeBuffer(signature).endCell(); +} +``` + +Now what is left is sending the message. The way we send created message is dependent on the message type. + +#### Sending external_signed message + +After obtaining signed message cell we need to serialize it as external message and send it to the blockchain. + +```ts +import { + Address, + beginCell, + Cell, + external, + storeMessage +} from '@ton/core'; + +// with authType = 'external' +const msgCell = createWalletTransferV5(...); + +const v5WalletAddress = Address.parse("EQ...."); + +const serializedExternal = external({ + to: v5WalletAddress, + body: msgCell +}); + +// boc is BoC, bag of cells +const boc = beginCell() + .store(storeMessage(serializedExternal)) + .endCell() + .toBoc(); +``` + +You can send this BoC to the network in any convenient way, e.g. with API provider like [TonCenter /message](https://toncenter.com/api/v3/index.html#/api%2Fv2/api_v3_post_v2_message) or with your own [liteserver](/ecosystem/node/overview). + +#### Sending internal_extension and internal_signed + +These two types of messages are internal messages, meaning that they need to come from another contract. However strange thay may sound, the easiest way to send internal message from another contract is to ask another wallet contract to send internal message to our wallet contract with body that will contain our constructed message. + + +```mermaid +graph LR + A["👤 User"] --> B["📱 V4 Wallet Contract
(Sender)"] + + B -->|"Internal message with body:
🔐 internal_signed (#73696e74)
or
🔧 internal_extension (#6578746e)"| C["🏦 V5 Wallet Contract
(Receiver)"] + + C --> D["⚡ Execute Actions
on V5 wallet"] + + classDef user fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + classDef v4wallet fill:#fff3e0,stroke:#f57c00,stroke-width:2px + classDef v5wallet fill:#e8f5e8,stroke:#388e3c,stroke-width:2px + classDef action fill:#ffebee,stroke:#d32f2f,stroke-width:2px + + class A user + class B v4wallet + class C v5wallet + class D action +``` + +For simplicity, we will use wallet V4 contract with existing client serialization library that will take care of all low level stuff. + +```ts expandable +import {beginCell, toNano, TonClient, WalletContractV4, internal, fromNano} from "@ton/ton" +import {getHttpEndpoint} from "@orbs-network/ton-access" +import {mnemonicToPrivateKey} from "@ton/crypto" + +// v4 wallet mnemonic +const mnemonics = "dog bacon bread ..." + +const endpoint = await getHttpEndpoint({network: 'testnet'}) +const client = new TonClient({ + endpoint: endpoint, +}) +const keyPair = await mnemonicToPrivateKey(mnemonics.split(" ")) +const secretKey = keyPair.secretKey +const workchain = 0 // we are working in basechain. +const deployerWallet = WalletContractV4.create({ + workchain: workchain, + publicKey: keyPair.publicKey, +}) + +const deployerWalletContract = client.open(deployerWallet) + + +// with authType = 'internal' or 'extension' +const internalMsgBody = createWalletTransferV5(...); + +// here we use function from previous section to create V5 wallet message cell +// next, we will send it as internal message using V4 wallet + +// address of our V5 wallet contract +const v5WalletAddress = Address.parse("EQ...."); + +const seqnoForV4Wallet: number = await deployerWalletContract.getSeqno() + +await deployerWalletContract.sendTransfer({ + seqno, + secretKey, + messages: [ + internal({ + to: v5WalletAddress, + value: toNano("0.05"), + body: internalMsgBody, + }), + ], +}) +``` diff --git a/standard/wallets/v5.mdx b/standard/wallets/v5.mdx index e69de29b..87240264 100644 --- a/standard/wallets/v5.mdx +++ b/standard/wallets/v5.mdx @@ -0,0 +1,143 @@ +--- +title: "Wallet standard V5" +sidebarTitle: "Overview" +--- + +This article provides overview of the latest wallet standard on TON blockchain - V5. + +The V5 wallet standard offers many benefits that improve the experience for both users and merchants. V5 supports gasless transactions, account delegation and recovery, subscription payments using tokens and Toncoin, and low-cost multi-transfers. In addition to retaining the previous functionality (V4), the new contract allows you to send up to 255 messages at a time. + +![v5](/resources/images/wallets/wallet-contract-V5.png) + +Wallet source code: + +- [ton-blockchain/wallet-contract-v5](https://github.com/ton-blockchain/wallet-contract-v5) + +TL-B scheme: + +- [ton-blockchain/wallet-contract-v5/types.tlb](https://github.com/ton-blockchain/wallet-contract-v5/blob/main/types.tlb) + +#### Official code hash + +| Contract version | Hash | +| ---------------- | ------------------------------------------------------------------ | +| V5 R1 | `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f` | + +#### Persistent memory layout + +```tlb +contract_state$_ + is_signature_allowed:(## 1) + seqno:# + wallet_id:(## 32) + public_key:(## 256) + extensions_dict:(HashmapE 256 int1) = ContractState; +``` + +As you can see, the `ContractState`, compared to previous versions, hasn't undergone major changes. The main difference is the new `is_signature_allowed` 1-bit flag, which restricts or allows access through the signature and stored public key. We will describe the importance of this change in later topics. + +#### Authentication process + +```tlb +signed_request$_ // 32 (opcode from outer) + wallet_id: # // 32 + valid_until: # // 32 + msg_seqno: # // 32 + inner: InnerRequest // + signature: bits512 // 512 += SignedRequest; // Total: 688 .. 976 + ^Cell + +internal_signed#73696e74 signed:SignedRequest = InternalMsgBody; + +internal_extension#6578746e + query_id:(## 64) + inner:InnerRequest = InternalMsgBody; + +external_signed#7369676e signed:SignedRequest = ExternalMsgBody; +``` + +Before we get to the actual payload of our messages — `InnerRequest` — let's first look at how version 5 differs from previous versions in the authentication process. The `InternalMsgBody` combinator describes two ways to access wallet actions through internal messages. The first method is one we are already familiar with from version 4: authentication as a previously registered extension, the address of which is stored in `extensions_dict`. The second method is authentication through the stored public key and signature, similar to external requests. + +At first, this might seem like an unnecessary feature, but it actually enables requests to be processed through external services (smart contracts) that are not part of your wallet's extension infrastructure — a key feature of V5. Gasless transactions rely on this functionality. + +Note that simply receiving funds is still an option. Practically, any received internal message that doesn't pass the authentication process will be considered a transfer. + +#### Actions + +The first thing that we should notice is `InnerRequest`, which we have already seen in the authentication process. In contrast to the previous version, both external and internal messages have access to the same functionality, except for changing the signature mode (i.e., the `is_signature_allowed` flag). + +```tlb +out_list_empty$_ = OutList 0; +out_list$_ {n:#} + prev:^(OutList n) + action:OutAction = OutList (n + 1); + +action_send_msg#0ec3c86d mode:(## 8) out_msg:^(MessageRelaxed Any) = OutAction; + +// Extended actions in V5: +action_list_basic$_ {n:#} actions:^(OutList n) = ActionList n 0; +action_list_extended$_ {m:#} {n:#} action:ExtendedAction prev:^(ActionList n m) = ActionList n (m+1); + +action_add_ext#02 addr:MsgAddressInt = ExtendedAction; +action_delete_ext#03 addr:MsgAddressInt = ExtendedAction; +action_set_signature_auth_allowed#04 allowed:(## 1) = ExtendedAction; + +actions$_ out_actions:(Maybe OutList) has_other_actions:(## 1) {m:#} {n:#} other_actions:(ActionList n m) = InnerRequest; +``` + +We can consider `InnerRequest` as two lists of actions: the first, `OutList`, is an optional chain of cell references, each containing a send message request led by the message mode. The second, `ActionList,` is led by a one-bit flag, `has_other_actions`, which marks the presence of extended actions, starting from the first cell and continuing as a chain of cell references. We are already familiar with the first two extended actions, `action_add_ext` and `action_delete_ext`, followed by the internal address that we want to add or delete from the extensions dictionary. The third, `action_set_signature_auth_allowed`, restricts or allows authentication through the public key, leaving the only way to interact with the wallet through extensions. This functionality might be extremely important in the case of a lost or compromised private key. + +#### Exit codes + +| Exit code | Description | +| --------- | ---------------------------------------------------------------------------------- | +| 132 | Authentication attempt through signature while it's disabled | +| 133 | `seqno` check failed, replay protection occurred | +| 134 | `wallet_id` does not correspond to the stored one | +| 135 | `Ed25519 signature` check failed | +| 136 | `valid-until` check failed | +| 137 | Enforce that `send_mode` has the +2 bit (ignore errors) set for external messages. | +| 138 | `external_signed` prefix doesn't correspond to the received one | +| 139 | Add extension operation was not successful | +| 140 | Remove extension operation was not successful | +| 141 | Unsupported extended message prefix | +| 142 | Tried to disable auth by signature while the extension dictionary is empty | +| 143 | Attempt to set signature to an already set state | +| 144 | Tried to remove the last extension when signature is disabled | +| 145 | Extension has the wrong workchain | +| 146 | Tried to change signature mode through external message | +| 147 | Invalid `c5`, `action_send_msg` verification failed | +| 0 | Standard successful execution exit code. | + + + +#### Get methods + +1. `int is_signature_allowed()` returns stored `is_signature_allowed` flag. +1. `int seqno()` returns current stored seqno. +1. `int get_wallet_id()` returns current wallet ID. +1. `int get_public_key()` returns current stored public key. +1. `cell get_extensions()` returns extensions dictionary. + +#### Preparing for gasless transactions + +Starting with v5, the wallet smart contract supports owner-signed internal messages (`internal_signed`), which enables gasless transactions—for example, paying network fees in USDt when transferring USDt. The common scheme looks like this: + +![Gasless transaction flow diagram](/resources/images/wallets/gasless.jpg) + +#### Flow + +1. When sending USDt, the user signs one message containing two outgoing USDt transfers: + 1. USDt transfer to the recipient's address. + 1. Transfer of a small amount of USDt in favor of the service. +1. This signed message is sent off-chain by HTTPS to the service backend. The service backend sends it to the TON Blockchain, paying Toncoin for network fees. + +The beta version of the gasless backend API is available at [tonapi.io/api-v2](https://tonapi.io/api-v2). If you are developing a wallet app and have feedback about these methods, please share it in [@tonapitech](https://t.me/tonapitech) chat. + +Wallet source code: + +- [ton-blockchain/wallet-contract-v5](https://github.com/ton-blockchain/wallet-contract-v5)