Working with wallet smart contracts
👋 Introduction
Learning how wallets and transactions work on TON before beginning smart contracts development is essential. This knowledge will help developers understand the interaction between wallets, messages, and smart contracts to implement specific development tasks.
Before starting this tutorial, we recommend reviewing the Wallet contracts article.
This section will teach us to create operations without using pre-configured functions to understand development workflows. The references chapter contains all the necessary references for analyzing this tutorial.
💡 Prerequisites
This tutorial requires basic knowledge of JavaScript and TypeScript or Go. It is also necessary to hold at least 3 TON (which can be stored in an exchange account, a non-custodial wallet, or the Telegram bot wallet). It is necessary to have a basic understanding of cell, blockchain of blockchains to understand this tutorial.
Working with the TON Testnet often leads to deployment errors, difficulty tracking transactions, and unstable network functionality. Completing most of the development on the TON Mainnet could help avoid potential issues. This may reduce the number of transactions and minimize fees.
💿 Source code
All code examples used in this tutorial can be found in the following GitHub repository.
✍️ What you need to get started
- Ensure NodeJS is installed.
- Specific Ton libraries are required and include: @ton/ton 13.5.1+, @ton/core 0.49.2+ and @ton/crypto 3.2.0+.
OPTIONAL: If you prefer Go instead of JS, install the tonutils-go library and the GoLand IDE to develop on TON. This library will be used in this tutorial for the Go version.
- JavaScript
- Go
npm i --save @ton/ton @ton/core @ton/crypto
go get github.com/xssnick/tonutils-go
go get github.com/xssnick/tonutils-go/adnl
go get github.com/xssnick/tonutils-go/address
⚙ Set your environment
To create a TypeScript project, you need to follow these steps in order:
- Create an empty folder (which we’ll name WalletsTutorial).
- Open the project folder using the CLI.
- Use the following commands to set up your project:
npm init -y
npm install typescript @types/node ts-node nodemon --save-dev
npx tsc --init --rootDir src --outDir build \ --esModuleInterop --target es2020 --resolveJsonModule --lib es6 \ --module commonjs --allowJs true --noImplicitAny false --allowSyntheticDefaultImports true --strict false
To help us carry out the following process, a ts-node
executes TypeScript code directly without precompiling. nodemon
restarts the node application automatically when file changes in the directory are detected.
- Next, remove these lines from
tsconfig.json
:
"files": [
"\\",
"\\"
]
- Then, create a
nodemon.json
config in your project root with the following content:
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
}
- Add this script to
package.json
instead of "test", which is included when the project is created.
"start:dev": "npx nodemon"
- Create a
src
folder in the project root and anindex.ts
file in this folder. - Next, the following code should be added:
async function main() {
console.log("Hello, TON!");
}
main().finally(() => console.log("Exiting..."));
- Run the code using the terminal:
npm run start:dev
- Finally, the console output will appear.
The TON Community created an excellent tool for automating all development processes (deployment, contract writing, testing) called Blueprint. However, we will not need such a powerful tool, so the instructions above should be followed.
OPTIONAL: When using Go, follow these instructions:
- Install the GoLand IDE.
- Create a project folder and a
go.mod
file with the following content. If the current version of Go is outdated, update it to the required version to proceed with this process:
module main
go 1.20
- Type the following command into the terminal:
go get github.com/xssnick/tonutils-go
- Create the
main.go
file in the root of your project with the following content:
package main
import (
"log"
)
func main() {
log.Println("Hello, TON!")
}
- Change the module's name in the
go.mod
tomain
. - Run the code above until the output in the terminal is displayed.
It is also possible to use another IDE since GoLand isn’t free, but it is preferred.
Add all coding components to the main
function created in the ⚙ Set your environment section.
Only the imports required for that specific code section are specified in each new section. Combine new imports with the existing ones as needed.
🚀 Let's get started!
In this tutorial, we’ll learn which wallets (versions 3 and 4) are most often used on TON Blockchain and get acquainted with how their smart contracts work. This will allow developers to better understand the different message types on the TON platform to make it simpler to create messages, send them to the blockchain, deploy wallets, and eventually, be able to work with high-load wallets.
Our main task is to build messages using various objects and functions for @ton/ton, @ton/core, and @ton/crypto (ExternalMessage, InternalMessage, Signing, etc.) to understand what messages look like on a bigger scale. To carry out this process, we'll use two main wallet versions (v3 and v4) because exchanges, non-custodial wallets, and most users only use these specific versions.
This tutorial may not explain particular details on occasion. In these cases, more details will be provided later.
IMPORTANT: Throughout this tutorial, the wallet v3 code is used to understand the wallet development process better. Version v3 has two sub-versions: r1 and r2. Currently, only the second version is being used, which means that when we refer to v3 in this document, it implies v3r2.
💎 TON Blockchain wallets
All wallets operating on the TON Blockchain are smart contracts, and everything running on TON functions as a smart contract. Like most blockchains, TON allows users to deploy and customize smart contracts for various purposes, enabling full wallet customization. Wallet smart contracts on TON facilitate communication between the platform and other types of smart contracts. However, it’s essential to understand how wallet communication works.
Wallet communication
Generally, TON Blockchain has two message types: internal
and external
. External messages allow sending messages to the blockchain from the outside world, thus allowing communication with smart contracts that accept such messages. The function responsible for carrying out this process is as follows:
() recv_external(slice in_msg) impure {
;; some code
}
Before exploring wallets in more detail, let’s examine how wallets accept external messages. On TON, every wallet stores the owner’s public key
, seqno
, and subwallet_id
. When a wallet receives an external message, it uses the get_data()
method to retrieve data from its storage. The wallet then performs several verification checks to determine whether to accept the message. This process works as follows:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; get signature from the message body
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); ;; get rest values from the message body
throw_if(35, valid_until <= now()); ;; check the relevance of the message
var ds = get_data().begin_parse(); ;; get data from storage and convert it into a slice to be able to read values
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); ;; read values from storage
ds.end_parse(); ;; make sure we do not have anything in ds variable
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, subwallet_id == stored_subwallet);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
💡 Useful links:
Now, let’s take a closer look.
Replay protection - seqno
Message replay protection in the wallet smart contract relies on the seqno
(Sequence Number), which tracks the order of sent messages. Preventing message repetition is critical, as duplicate messages can compromise the system’s integrity. When analyzing wallet smart contract code, the seqno
is typically managed as follows:
throw_unless(33, msg_seqno == stored_seqno);
The code above compares the seqno
from the incoming message with the seqno
stored in the smart contract. If the values do not match, the contract returns an error with the 33 exit code
. This ensures that if the sender provides an invalid seqno
, indicating a mistake in the message sequence, the contract prevents further processing and safeguards against such errors.
It's also essential to consider that anyone can send external messages. If you send 1 TON to someone, someone else can repeat this message. However, when the seqno
increases, the previous external message becomes invalid, and no one will be able to repeat it, thus preventing the possibility of stealing your funds.
Signature
As mentioned earlier, wallet smart contracts accept external messages. However, since these messages originate from the outside world, their data cannot be fully trusted. Therefore, each wallet stores the owner's public key. When the wallet receives an external message signed with the owner’s private key, the smart contract uses the public key to verify the message’s signature. This ensures the message genuinely comes from the contract owner.
The wallet first extracts the signature from the incoming message to perform this verification. It then loads the public key from storage and validates the signature using the following process:
var signature = in_msg~load_bits(512);
var ds = get_data().begin_parse();
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
If all verification steps succeed, the smart contract accepts and processes the message:
accept_message();
Since external messages do not include the Toncoin required to pay transaction fees, the accept_message()
function applies a gas_credit
(currently valued at 10,000 gas units). This allows the contract to perform necessary calculations for free, provided the gas usage does not exceed the gas_credit
limit. After invoking accept_message()
, the smart contract deducts all gas costs (in TON) from its balance. You can read more about this process here.
Transaction expiration
Another step used to check the validity of external messages is the valid_until
field. As you can see from the variable name, this is the time in UNIX before the message is valid. If this verification process fails, the contract completes the processing of the transaction and returns the 35 exit code as follows:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(35, valid_until <= now());
This algorithm safeguards against potential errors, such as when a message is no longer valid but is still sent to the blockchain for an unknown reason.
Wallet v3 and wallet v4 differences
The key difference between wallet v3 and wallet v4 lies in wallet v4’s support for plugins
. Users can install or delete these plugins, which are specialized smart contracts capable of requesting a specific amount of TON from the wallet smart contract at a designated time.
Wallet smart contracts automatically send the required amount of TON in response to plugin requests without requiring the owner’s involvement. This functionality mirrors a subscription model, which is the primary purpose of plugins. We won’t delve into these details further as they fall outside the scope of this tutorial.
How wallets facilitate communication with smart contracts
As mentioned, a wallet smart contract accepts external messages, validates them, and processes them if all checks pass. Once the contract accepts a message, it begins a loop to extract messages from the body of the external message, creates internal messages, and sends them to the blockchain as shown below:
cs~touch();
while (cs.slice_refs()) {
var mode = cs~load_uint(8); ;; load message mode
send_raw_message(cs~load_ref(), mode); ;; get each new internal message as a cell with the help of load_ref() and send it
}
On TON, all smart contracts run on the stack-based TON Virtual Machine (TVM). ~ touch() places the variable cs
on top of the stack to optimize code running for less gas.
Since a single cell can store a maximum of 4 references, we can send up to 4 internal messages per external message.
💡 Useful links:
📬 External and internal messages
This section will explore internal
and external
messages in more detail. We’ll create and send these messages to the network, minimizing reliance on pre-built functions.
To simplify this process, we’ll use a pre-built wallet. Here’s how to proceed:
- Install the wallet app (e.g., Tonkeeper is used by the author)
- Switch the wallet app to v3r2 address version
- Deposit 1 TON into the wallet
- Send the message to another address (you can send it to yourself, to the same wallet).
This way, the Tonkeeper wallet app will deploy the wallet contract, which we can use for the following steps.
At the time of writing, most wallet apps on TON default to wallet v4. However, since plugins are not required for this tutorial, we’ll use the functionality provided by wallet v3. Tonkeeper allows users to select their preferred wallet version, so it’s recommended to deploy wallet v3.
TL-B
As mentioned earlier, everything in the TON Blockchain is a smart contract composed of cells. Standards are essential to ensure proper serialization and deserialization of data. For this purpose, TL-B
was developed as a universal tool to describe various data types, structures, and sequences within cells.
This section will explore block.tlb. This file will be invaluable for future development as it outlines how to assemble different types of cells. Specifically for our purposes, it provides detailed information about the structure and behavior of internal and external messages.
This guide provides basic information. For further details, please refer to our TL-B documentation to learn more about TL-B.
CommonMsgInfo
Initially, each message must first store CommonMsgInfo
(TL-B) or CommonMsgInfoRelaxed
(TL-B). This allows us to define technical details that relate to the message type, message time, recipient address, technical flags, and fees.
By reading the block.tlb
file, we can notice three types of CommonMsgInfo: int_msg_info$0
, ext_in_msg_info$10
, ext_out_msg_info$11
. We will not go into specific details detailing the specificities of the ext_out_msg_info
TL-B structure. That said, it is an external message type that a smart contract can send to use as an external log. For examples of this format, consider having a closer look at the Elector contract.
When examining TL-B, you’ll notice that only CommonMsgInfo
is available when using the ext_in_msg_info
type. It is because fields like src
, created_lt
, created_at
, and others are overwritten by validators during transaction processing. Among these, the src
field is particularly important. Since the sender’s address is unknown when the message is sent, validators populate this field during verification. This ensures the src
address is accurate and cannot be tampered with.
However, the CommonMsgInfo
structure only supports the MsgAddress
specification. Since the sender’s address is typically unknown, it’s necessary to use addr_none
(represented by two zero bits 00
). The CommonMsgInfoRelaxed
structure is used in such cases, as it supports the addr_none
address. For ext_in_msg_info
(used for incoming external messages), the CommonMsgInfo
structure is sufficient because these messages don’t require a sender and always use the MsgAddressExt structure (represented by addr_none$00
, meaning two zero bits). This eliminates the need to overwrite the data.
The numbers after the $
symbol are the bits that must be stored at the beginning of a specific structure for further identification of these structures during reading (deserialization).
Internal message creation
Internal messages facilitate communication between contracts. When examining various contract types, such as NFTs and Jettons, you’ll often encounter the following lines of code, which are commonly used when writing contracts that send messages:
var msg = begin_cell()
.store_uint(0x18, 6) ;; or 0x10 for non-bounce
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
Let’s examine 0x18
and 0x10
(where x
denotes hexadecimal). These numbers can be represented in binary as 011000
and 010000
, assuming we allocate 6 bits. This means the code above can be rewritten as follows:
var msg = begin_cell()
.store_uint(0, 1) ;; this bit indicates that we send an internal message according to int_msg_info$0
.store_uint(1, 1) ;; IHR Disabled
.store_uint(1, 1) ;; or .store_uint(0, 1) for 0x10 | bounce
.store_uint(0, 1) ;; bounced
.store_uint(0, 2) ;; src -> two zero bits for addr_none
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
Now, let’s go through each option in detail:
Option | Explanation |
---|---|
IHR Disabled | Currently, this option is disabled (meaning we store 1 ) because Instant Hypercube Routing (IHR) is not yet fully implemented. This option will become relevant once many Shardchains are active on the network. For more details about the IHR Disabled option, refer to tblkch.pdf (chapter 2). |
Bounce | When sending messages, errors can occur during smart contract processing. Setting the Bounce option to 1 (true) is essential to prevent TON loss. If any errors arise during transaction processing, the message will be returned to the sender, and the same amount of TON (minus fees) will be refunded. Refer to this guide for more details on non-bounceable messages. |
Bounced | Bounced messages are those returned to the sender due to an error during transaction processing with a smart contract. This option indicates whether the received message is bounced or not. |
Src | The Src is the sender's address. In this case, two zero bits indicate the addr_none address. |
The following two lines of code:
...
.store_slice(to_address)
.store_coins(amount)
...
- we specify the recipient and the number of TON to be sent.
Finally, let’s look at the remaining lines of code:
...
.store_uint(0, 1) ;; Extra currency
.store_uint(0, 4) ;; IHR fee
.store_uint(0, 4) ;; Forwarding fee
.store_uint(0, 64) ;; Logical time of creation
.store_uint(0, 32) ;; UNIX time of creation
.store_uint(0, 1) ;; State Init
.store_uint(0, 1) ;; Message body
;; store something as a body
Option | Explanation |
---|---|
Extra currency | This is a native implementation of existing jettons and is not currently in use. |
IHR fee | As mentioned, IHR is not currently used, so this fee is always zero. For more information, refer to tblkch.pdf (section 3.1.8). |
Forwarding fee | A forwarding message fee. For more information, refer to fees documentation. |
Logical time of creation | The time used to create the correct messages queue. |
UNIX timestamp of creation | The time the message was created in UNIX. |
State Init | The code and source data for deploying a smart contract. If the bit is set to 0 , there is no State Init. However, if it’s set to 1 , an additional bit is required to indicate whether the State Init is stored in the same cell (0 ) or written as a reference (1 ). |
Message body | This section determines how the message body is stored. If the message body is too large to fit directly into the message, it is stored as a reference. In this case, the bit is set to 1 to indicate that the body is stored as a reference. If the bit is 0 , the body resides in the same cell as the message. |
Validators rewrite the above values (including src), excluding the State Init and the Message Body bits.
If the number value fits within fewer bits than is specified, then the missing zeros are added to the left side of the value. For example, 0x18 fits within 5 bits -> 11000
. However, since 6 bits were specified, the result becomes 011000
.
Next, we’ll prepare a message to send Toncoins to another wallet v3. For example, let’s say a user wants to send 0.5 TON to themselves with the comment "Hello, TON!". To learn how to send a message with a comment, refer to this documentation section: How to send a simple message.
- JavaScript
- Go
import { beginCell } from "@ton/core";
let internalMessageBody = beginCell()
.storeUint(0, 32) // write 32 zero bits to indicate that a text comment will follow
.storeStringTail("Hello, TON!") // write our text comment
.endCell();
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32). // write 32 zero bits to indicate that a text comment will follow
MustStoreStringSnake("Hello, TON!"). // write our text comment
EndCell()
Above, we created an InternalMessageBody
to store the body of our message. Note that if the text exceeds the capacity of a single Cell (1023 bits), it’s necessary to split the data into multiple cells, as outlined in this documentation. However, high-level libraries handle cell creation according to the requirements in this case, so there’s no need to worry about it at this stage.
Next, the InternalMessage
is created according to the information we have studied earlier as follows:
- JavaScript
- Go
import { toNano, Address } from "@ton/ton";
const walletAddress = Address.parse("put your wallet address");
let internalMessage = beginCell()
.storeUint(0, 1) // indicate that it is an internal message -> int_msg_info$0
.storeBit(1) // IHR Disabled
.storeBit(1) // bounce
.storeBit(0) // bounced
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress)
.storeCoins(toNano("0.2")) // amount
.storeBit(0) // Extra currency
.storeCoins(0) // IHR Fee
.storeCoins(0) // Forwarding Fee
.storeUint(0, 64) // Logical time of creation
.storeUint(0, 32) // UNIX time of creation
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(internalMessageBody) // Store Message Body as a reference
.endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
walletAddress := address.MustParseAddr("put your address")
internalMessage := cell.BeginCell().
MustStoreUInt(0, 1). // indicate that it is an internal message -> int_msg_info$0
MustStoreBoolBit(true). // IHR Disabled
MustStoreBoolBit(true). // bounce
MustStoreBoolBit(false). // bounced
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress).
MustStoreCoins(tlb.MustFromTON("0.2").NanoTON().Uint64()). // amount
MustStoreBoolBit(false). // Extra currency
MustStoreCoins(0). // IHR Fee
MustStoreCoins(0). // Forwarding Fee
MustStoreUInt(0, 64). // Logical time of creation
MustStoreUInt(0, 32). // UNIX time of creation
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(internalMessageBody). // Store Message Body as a reference
EndCell()
Creating a message
We must create a client
to retrieve our wallet smart contract's seqno
(sequence number). This client will send a request to execute the Get method seqno
on our wallet. Additionally, we must include the seed phrase (saved during wallet creation here) to sign our message. Follow these steps to proceed:
- JavaScript
- Go
import { TonClient } from "@ton/ton";
import { mnemonicToWalletKey } from "@ton/crypto";
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC", // you can replace it on https://testnet.toncenter.com/api/v2/jsonRPC for testnet
apiKey: "put your api key", // you can get an api key from @tonapibot bot in Telegram
});
const mnemonic = "put your mnemonic"; // word1 word2 word3
let getMethodResult = await client.runMethod(walletAddress, "seqno"); // run "seqno" GET method from your wallet contract
let seqno = getMethodResult.stack.readNumber(); // get seqno from response
const mnemonicArray = mnemonic.split(" "); // get array from string
const keyPair = await mnemonicToWalletKey(mnemonicArray); // get Secret and Public keys from mnemonic
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"golang.org/x/crypto/pbkdf2"
"log"
"strings"
)
mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection) // create client
block, err := client.CurrentMasterchainInfo(context.Background()) // get the current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
// The next three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. With the tonutils-go library, this is all implemented, but we’re doing it again to get a full understanding.
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries, "TON default seed" is used as salt when getting keys
privateKey := ed25519.NewKeyFromSeed(k)
To proceed, we must send the seqno
, keys
, and internal message
. Next, we’ll create a message for our wallet and store the data in the sequence outlined at the beginning of the tutorial. This is achieved as follows:
- JavaScript
- Go
import { sign } from "@ton/crypto";
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id | We consider this further
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32) // store seqno
.storeUint(3, 8) // store mode of our internal message
.storeRef(internalMessage); // store our internalMessage as a reference
let signature = sign(toSign.endCell().hash(), keyPair.secretKey); // get the hash of our message to the wallet smart contract and sign it to get signature
let body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
import (
"time"
)
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // Message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
MustStoreUInt(uint64(3), 8). // store mode of our internal message
MustStoreRef(internalMessage) // store our internalMessage as a reference
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash()) // get the hash of our message to the wallet smart contract and sign it to get the signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
Note that no .endCell()
was used in defining the toSign
here. In this case, it is necessary to transfer toSign content directly to the message body. If writing a cell was required, it would have to be stored as a reference.
In addition to the basic verification process we learned above for the Wallet V3, Wallet V4 smart contracts extract the opcode to determine whether a simple transaction or a message associated with the plugin is required. To match this version, it is necessary to add the storeUint(0, 8)
(JS/TS), MustStoreUInt(0, 8)
(Go) functions after writing the sequence number (seqno) and before specifying the transaction mode.
External message creation
To deliver an internal message to the blockchain from the outside world, it must be sent within an external message. As previously discussed, we’ll use the ext_in_msg_info$10
structure since our goal is to send an external message to our contract. Now, let’s create the external message that will be sent to our wallet:
- JavaScript
- Go
let externalMessage = beginCell()
.storeUint(0b10, 2) // 0b10 -> 10 in binary
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress) // Destination address
.storeCoins(0) // Import Fee
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // 0b10 -> 10 in binary
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
Option | Explanation |
---|---|
Src | The sender address. Since an incoming external message cannot have a sender, there will always be 2 zero bits (an addr_none TL-B). |
Import Fee | The fee for importing incoming external messages. |
State Init | Unlike the Internal Message, the State Init within the external message is needed to deploy a contract from the outside world. The State Init used with the Internal Message allows one contract to deploy another. |
Message Body | The message must be sent to the contract for processing. |
0b10 (b - binary) denotes a binary record. Two bits are stored in this process: 1
and 0
. Thus, we specify that it's ext_in_msg_info$10
.
Now that we have a completed message ready to send to our contract, the next step is to serialize it into a BoC
(bag of cells). Once serialized, we can send it using the following code:
- JavaScript
- Go
console.log(externalMessage.toBoc().toString("base64"));
client.sendFile(externalMessage.toBoc());
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tl"
)
log.Println(base64.StdEncoding.EncodeToString(externalMessage.ToBOCWithFlags(false)))
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
💡 Useful link:
More about Bag of cells
As a result, we got the output of our BOC in the console, and the message was sent to our wallet. By copying the base64 encoded string, it is possible to manually send our message and retrieve the hash using toncenter.
👛 Deploying a wallet
We’ve covered the basics of creating messages to help us deploy a wallet. Previously, we deployed wallets using wallet apps, but we’ll deploy our wallet manually this time.
In this section, we’ll walk through creating a wallet (wallet v3) from scratch. You’ll learn how to compile the wallet smart contract code, generate a mnemonic phrase, obtain a wallet address, and deploy the wallet using external messages and State Init (state initialization).
Generating a mnemonic
The first step in creating a wallet is generating a private
and public
key. We’ll generate a mnemonic seed phrase and extract the keys using cryptographic libraries.
Here’s how to accomplish this:
- JavaScript
- Go
import { mnemonicToWalletKey, mnemonicNew } from "@ton/crypto";
// const mnemonicArray = 'put your mnemonic'.split(' ') // get our mnemonic as array
const mnemonicArray = await mnemonicNew(24); // 24 is the number of words in a seed phrase
const keyPair = await mnemonicToWalletKey(mnemonicArray); // extract private and public keys from mnemonic
console.log(mnemonicArray); // if we want, we can print our mnemonic
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"log"
"github.com/xssnick/tonutils-go/ton/wallet"
"golang.org/x/crypto/pbkdf2"
"strings"
)
// mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
mnemonic := wallet.NewSeed() // get new mnemonic
// The following three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. It has all been implemented in the tonutils-go library, but it immediately returns the finished wallet object with the address and ready methods. So we’ll have to write the lines to get the key separately. GoLand IDE will automatically import all required libraries (crypto, pbkdf2, and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries, "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
publicKey := privateKey.Public().(ed25519.PublicKey) // get public key from private key
log.Println(publicKey) // print publicKey so that at this stage, the compiler does not complain that we do not use our variable
log.Println(mnemonic) // if we want, we can print our mnemonic
The private key is needed to sign messages, and the public key is stored in the wallet’s smart contract.
Make sure to output the generated mnemonic seed phrase to the console, save it, and use it (as detailed in the previous section) to ensure the same key pair is used each time the wallet’s code is run.
Subwallet IDs
One of the most notable benefits of wallets being smart contracts is the ability to create a vast number of wallets using just one private key. This is because the addresses of smart contracts on TON Blockchain are computed using several factors, including the stateInit
. The stateInit contains the code
and initial data
, which is stored in the blockchain’s smart contract storage.
Changing just one bit within the stateInit can generate a different address. That is why the subwallet_id
was initially created. The subwallet_id
is stored in the contract storage and can be used to create many different wallets (with different subwallet IDs) with one private key. This functionality can be handy when integrating various wallet types with centralized services such as exchanges.
The default subwallet_id
value is 698983191
, as per the line of code below taken from the TON Blockchain’s source code:
res.wallet_id = td::as<td::uint32>(res.config.zero_state_id.root_hash.as_slice().data());
It is possible to retrieve genesis block information (zero_state) from the configuration file. Understanding the complexities and details of this is not necessary, but it's important to remember that the default value of the subwallet_id
is 698983191
.
Each wallet contract checks the subwallet_id
field for external messages to avoid instances where requests are sent to a wallet with another ID:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(34, subwallet_id == stored_subwallet);
We will need to add the above value to the initial data of the contract, so the variable needs to be saved as follows:
- JavaScript
- Go
const subWallet = 698983191;
var subWallet uint64 = 698983191
Compiling wallet code
Now that the private and public keys and the subwallet_id
are clearly defined, we must compile the wallet code. We’ll use the wallet v3 code from the official repository.
The @ton-community/func-js library is necessary to compile wallet code. This library allows us to compile FunC code and retrieve a cell containing the code. To get started, install the library and save it to the package.json
as follows:
npm i --save @ton-community/func-js
We’ll only use JavaScript to compile code, as the libraries for compiling code are JavaScript-based. However, after compiling is finalized, as long as we have our cell's base64 output, it is possible to use this compiled code in languages such as Go and others.
First, we must create two files: wallet_v3.fc
and stdlib.fc
. The compiler relies on the stdlib.fc
library, which contains all the necessary basic functions corresponding to asm
instructions. You can download the stdlib.fc
file here. For the wallet_v3.fc
file, copy the code from the repository.
Now, we have the following structure for the project we are creating:
.
├── src/
│ ├── main.ts
│ ├── wallet_v3.fc
│ └── stdlib.fc
├── nodemon.json
├── package-lock.json
├── package.json