diff --git a/standard/wallets/how-it-works.mdx b/standard/wallets/how-it-works.mdx index 43023d01e..38d7a8ca0 100644 --- a/standard/wallets/how-it-works.mdx +++ b/standard/wallets/how-it-works.mdx @@ -56,7 +56,7 @@ It also makes retries safe: a client may re-send the same signed message until t `subwallet_id` is a 32‑bit identifier tied to a wallet instance. It is set at wallet creation time and is included in the signing message. On each external call, the wallet verifies that the provided `subwallet_id` matches its own; otherwise, the message is rejected. -By varying `subwallet_id`, you can deploy multiple independent wallet contracts controlled by the same public key, each with its own address and `seqno`. This is useful for separating accounts, environments, or business lines while keeping a single keypair. +Multiple independent wallet contracts can share the same public key by using different `subwallet_id` values, each with its own address and `seqno`. This is useful for separating accounts, environments, or business lines while keeping a single keypair. + + diff --git a/standard/wallets/interact.mdx b/standard/wallets/interact.mdx index 36f39ab10..bbd174eb7 100644 --- a/standard/wallets/interact.mdx +++ b/standard/wallets/interact.mdx @@ -36,39 +36,50 @@ This means that the mnemonic alone does not determine an address uniquely: `wall For Wallet V4R2 `wallet_id` is defined as first 4 bytes from TON mainnet blockchain initial state hash. There is no specific logic why this number was chosen, community needed some default value and this one works well enough. -For Wallet V5, `wallet_id` is different between Mainnet and Testnet for security reasons. There will be different wallet addresses for different networks with the same mnemonic. This prevents replaying transactions from testnet on mainnet, and ensuing loss of funds. The `wallet_id` value is obtained from +For Wallet V5, `wallet_id` is different between mainnet and testnet for security reasons. Wallet addresses for the same mnemonic differ across networks. This prevents replaying transactions from testnet on mainnet and ensuing loss of funds. The `wallet_id` value is obtained from -- `network_id`: `-239` for mainnet, `-3` for testnet, -- `wallet_version`: currently always `0`, -- `subwallet_number`: `0` by default, -- `workchain`: `0` for basechain +- `network_id`: `-239` for mainnet, `-3` for testnet; +- `wallet_version`: currently always `0`; +- `subwallet_number`: `0` by default; +- `workchain`: `0` for basechain; -by the following algorithm: +using the following derivation scheme (client context): ```ts -type WalletIdV5 = { - // currently always 0 - readonly walletVersion: number; - // -239 for mainnet, -3 for testnet - readonly networkGlobalId: number; - // 0 for basechain - readonly workchain: number; - // 0 for the first wallet with this mnemonic - readonly subwalletNumber: number; -} +import { beginCell, Builder } from '@ton/core'; + +type WalletIdV5ClientContext = { + // -239 for mainnet, -3 for testnet + readonly networkGlobalId: number; + // 0 for basechain + readonly workchain: number; + // currently always 0 for V5R1 + readonly walletVersion: number; + // 0 for the first wallet with this mnemonic + readonly subwalletNumber: number; // 0..2^15-1 +}; -export function storeWalletIdV5(walletId: WalletIdV5) { - return (builder: Builder) => { - builder.storeInt(walletId.networkGlobalId, 32); - builder.storeInt(walletId.workchain, 8); - builder.storeUint(walletId.walletVersion, 8); - builder.storeUint(walletId.subwalletNumber, 32); - } +export function storeWalletIdV5R1(ctx: WalletIdV5ClientContext) { + return (builder: Builder) => { + // context_id_client$1 = wc:int8 wallet_version:uint8 counter:uint15 + const context = beginCell() + .storeUint(1, 1) + .storeInt(ctx.workchain, 8) + .storeUint(ctx.walletVersion, 8) + .storeUint(ctx.subwalletNumber, 15) + .endCell() + .beginParse() + .loadInt(32); + + const walletId = ctx.networkGlobalId ^ context; + + builder.storeInt(walletId, 32); + }; } ``` ### Examples diff --git a/standard/wallets/v5-api.mdx b/standard/wallets/v5-api.mdx index dc9f55c84..8f288768e 100644 --- a/standard/wallets/v5-api.mdx +++ b/standard/wallets/v5-api.mdx @@ -411,14 +411,14 @@ export function createWalletTransferV5R1( // 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.validUntil, 32); signingMessage .storeUint(args.seqno, 32) .store(storeOutListExtendedV5(args.actions)); // now we need to sign message - const signature = sign(signingMessage.endCell().hash(), args.secretKey); + const signature = sign(signingMessage.endCell().hash(), args.privateKey); return beginCell().storeBuilder(signingMessage).storeBuffer(signature).endCell(); } diff --git a/standard/wallets/v5.mdx b/standard/wallets/v5.mdx index 96202e4c7..d49524fe9 100644 --- a/standard/wallets/v5.mdx +++ b/standard/wallets/v5.mdx @@ -27,11 +27,27 @@ contract_state$_ - `is_signature_allowed`: 1-bit flag that restricts or allows access through the signature and stored public key. - `seqno`: 32-bit sequence number. -- `wallet_id`: 32-bit wallet ID (equivalent to subwallet\_id in previous versions). +- `wallet_id`: 32-bit wallet ID (see the wallet ID scheme below). - `public_key`: 256-bit public key. - `extensions_dict`: dictionary containing extensions (may be empty). -As you can see, the `ContractState`, compared to previous versions, hasn't changed much. 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. +## Wallet ID scheme + +In Wallet V5 the `wallet_id` field is a 32-bit signed integer derived from the network global identifier and a 32-bit context value: + +- `wallet_id = network_global_id ^ context_id` + +The scheme used by the reference implementation and official wrapper is: + +- `network_global_id`: 32-bit network identifier (`-239` for mainnet, `-3` for testnet). +- `context_id`: 32-bit value that encodes either a client context or a custom context: + - Client context: `context_id_client$1 = wc:int8 wallet_version:uint8 counter:uint15` + - `wc`: workchain identifier. + - `wallet_version`: version discriminator; for Wallet V5R1 it is `0`. + - `counter`: 15-bit subwallet number. + - Custom context: `context_id_backoffice$0 = counter:uint31`, reserved for specialized infrastructure and back-office uses. + +For compatibility, the get-method is still named `get_subwallet_id()`, but in Wallet V5 it returns the derived `wallet_id` value described above rather than a plain `subwallet_id`. ## Message layout @@ -72,6 +88,15 @@ We can consider `InnerRequest` as two lists of actions: the first, `OutList`, is Learn more about actions [here](/standard/wallets/v5-api). +### Action list validation + +Wallet V5 allows precomputed outgoing actions to be placed in the TVM `C5` register as a raw `OutList`. Before executing them, the contract validates `C5` (see `verify_c5_actions` in `wallet_v5.fc`): + +- only `action_send_msg` actions are allowed; actions such as `set_code`, `reserve_currency`, or `change_library` are rejected; +- each action must contain an 8-bit `send_mode` and exactly two references (next-action reference and `MessageRelaxed` reference); +- for external messages, `send_mode` must have the `+2` (“ignore errors”) bit set; otherwise exit code `137` is thrown; +- the number of actions is limited to 255; exceeding this bound or violating the structure results in exit code `147` (`invalid_c5`). + ## Exit codes | Exit code | Description | @@ -104,7 +129,7 @@ Learn more about actions [here](/standard/wallets/v5-api). 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_subwallet_id()` returns current `wallet_id` value (for Wallet V5 this is the derived identifier described in the wallet ID scheme). 1. `int get_public_key()` returns current stored public key. 1. `cell get_extensions()` returns extensions dictionary.