Understanding how wallet smart contracts function on The Open Network (TON) is essential for any developer aiming to build decentralized applications or interact directly with the blockchain. Unlike traditional wallets, TON wallets are intelligent, programmable contracts that manage transaction logic, security, and communication. This guide dives deep into the mechanics of wallet operations, message handling, and contract deployment—without relying on pre-built functions—giving you full control and insight into the development workflow.
By mastering these core concepts, you'll be equipped to create secure, efficient, and scalable applications on TON. Whether you're building a wallet interface, deploying custom contracts, or processing bulk transactions, this knowledge forms the foundation of robust blockchain development.
Prerequisites for Development
Before diving into wallet interactions, ensure you meet the following prerequisites:
- Programming knowledge: Familiarity with JavaScript/TypeScript or Golang.
- Basic TON concepts: Understanding of cells, addresses, and the blockchain-of-blockchains architecture.
- Funds: At least 3 TON for testing—stored in a non-custodial wallet, exchange, or Telegram-based wallet.
💡 Why use Mainnet?
While testnets are useful, they often suffer from instability, delayed transactions, and deployment errors. For reliable results and realistic gas behavior, conduct most of your development on TON Mainnet.
Required Libraries
Install the necessary TON SDK packages:
npm i --save @ton/ton @ton/core @ton/cryptoFor Go developers, use:
go get github.com/xssnick/tonutils-go👉 Discover how to integrate smart contract logic into real-world applications.
Setting Up Your Development Environment
To begin, initialize a TypeScript project:
- Create a new folder:
WalletsTutorial Run:
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 falseAdd
nodemon.json:{ "watch": ["src"], "ext": ".ts,.js", "exec": "npx ts-node ./src/index.ts" }Update
package.jsonwith:"scripts": { "start:dev": "npx nodemon" }Create
src/index.ts:async function main() { console.log("Hello, TON!"); } main().finally(() => console.log("Exiting..."));
Run npm run start:dev to verify the setup.
🛠️ Optional Tool: Blueprint automates contract deployment and testing but isn't needed here—we're focusing on low-level understanding.
Understanding Wallet Smart Contracts
All wallets on TON are smart contracts. They handle incoming messages, verify signatures, manage sequence numbers (seqno), and forward internal transactions. This programmability enables fully customizable wallets.
How Wallets Communicate
TON supports two message types:
- Internal messages: Between smart contracts.
- External messages: From off-chain sources (e.g., wallets, dApps).
Wallets accept external messages via the recv_external(slice in_msg) function. Upon receipt, they:
- Extract signature, seqno, subwallet ID.
- Retrieve stored public key and seqno.
- Validate message authenticity and sequence.
Example validation logic:
var signature = in_msg~load_bits(512);
var cs = in_msg;
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());
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(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();Replay Protection with Seqno
The seqno field prevents message replay attacks. Each transaction must increment this number. If an old seqno is reused, the contract rejects it with exit code 33.
This ensures that even if someone intercepts your signed message, they cannot resend it after the first execution.
Signature Verification
Wallets verify ownership using digital signatures:
var signature = in_msg~load_bits(512);
var ds = get_data().begin_parse();
var public_key = ds~load_uint(256);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));Only messages signed with the correct private key will pass verification.
Transaction Expiry
The valid_until field sets a deadline for message processing. If the current time exceeds this value, the message is rejected (exit code 35). This prevents stale transactions from being executed.
Wallet v3 vs Wallet v4
The primary difference lies in plugins:
- Wallet v3: Basic functionality.
- Wallet v4: Supports plugins—smart contracts that can request funds automatically (e.g., subscription models).
For simplicity, we’ll focus on Wallet v3 (specifically v3r2).
Building Internal Messages Manually
Internal messages transfer value or data between contracts. Here’s how to construct one:
import { beginCell, toNano, Address } from '@ton/core';
const internalMessageBody = beginCell()
.storeUint(0, 32)
.storeStringTail("Hello, TON!")
.endCell();
const internalMessage = beginCell()
.storeUint(0x18, 6) // bounceable
.storeAddress(Address.parse('UQ...'))
.storeCoins(toNano("0.2"))
.storeBit(0) // no extra currency
.storeCoins(0) // IHR fee
.storeCoins(0) // forwarding fee
.storeUint(0, 64) // logical time
.storeUint(0, 32) // UNIX time
.storeBit(0) // no state init
.storeBit(1) // body as reference
.storeRef(internalMessageBody)
.endCell();Each field corresponds to specific transaction metadata required by the blockchain.
Creating External Messages for Deployment
To deploy a wallet or send a transaction from off-chain, wrap internal logic in an external message.
Step 1: Retrieve Seqno
const client = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC" });
const getMethodResult = await client.runMethod(walletAddress, "seqno");
const seqno = getMethodResult.stack.readNumber();Step 2: Sign the Message
import { sign } from '@ton/crypto';
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id
.storeUint(Math.floor(Date.now()/1e3) + 60, 32) // expiration
.storeUint(seqno, 32)
.storeUint(3, 8)
.storeRef(internalMessage);
const signature = sign(toSign.endCell().hash(), keyPair.secretKey);Step 3: Build External Message
const body = beginCell()
.storeBuffer(signature)
.storeBuilder(toSign)
.endCell();
const externalMessage = beginCell()
.storeUint(0b10, 2)
.storeUint(0, 2) // src addr_none
.storeAddress(walletAddress)
.storeCoins(0)
.storeBit(0) // no state init
.storeBit(1)
.storeRef(body)
.endCell();Step 4: Send to Blockchain
client.sendFile(externalMessage.toBoc());👉 Learn how to optimize gas usage when deploying smart contracts.
Deploying a Wallet from Scratch
You can deploy a wallet contract manually using compiled FunC code.
Generate Mnemonic and Keys
import { mnemonicNew, mnemonicToWalletKey } from '@ton/crypto';
const mnemonicArray = await mnemonicNew(24);
const keyPair = await mnemonicToWalletKey(mnemonicArray);Compile Wallet Code
Use @ton-community/func-js to compile .fc files:
import { compileFunc } from '@ton-community/func-js';
const result = await compileFunc({ targets: ['wallet_v3.fc'] });
const codeCell = Cell.fromBoc(Buffer.from(result.codeBoc, 'base64'))[0];Construct State Init
State Init defines initial contract state:
const dataCell = beginCell()
.storeUint(0, 32) // seqno
.storeUint(698983191, 32) // subwallet_id
.storeBuffer(keyPair.publicKey)
.endCell();
const stateInit = beginCell()
.storeBit(0) // split_depth
.storeBit(0) // special
.storeBit(1) // code present
.storeRef(codeCell)
.storeBit(1) // data present
.storeRef(dataCell)
.storeBit(0) // no library
.endCell();Compute Wallet Address
const contractAddress = new Address(0, stateInit.hash());
console.log(`Wallet address: ${contractAddress.toString()}`);Deploy by sending coins to this address followed by a signed initialization message.
Sending Multiple Messages at Once
A single cell can hold up to 4 references, allowing you to send up to four internal messages in one external transaction.
Loop through recipients and build messages:
for (let i = 0; i < destinations.length; i++) {
toSign.storeUint(3, 8).storeRef(messages[i]);
}This is ideal for batch transfers or notifications.
High-Load Wallets for Scalability
For high-frequency operations (e.g., exchanges), use Highload Wallet V3, which supports thousands of queries via query_id instead of seqno.
Query ID Mechanism
Unlike seqno, which requires sequential updates, query_id allows parallel processing:
- Each message uses a unique
(shift, bit_number)pair. - Processed queries are tracked in dictionaries (
queries,old_queries). - Old entries are purged based on
timeout.
This design avoids bottlenecks during mass withdrawals.
Deployment Example
Using wrapper libraries simplifies interaction:
import { HighloadWalletV3 } from "./wrappers/HighloadWalletV3";
const wallet = client.open(HighloadWalletV3.createFromConfig({
publicKey: keyPair.publicKey,
subwalletId: 0x10ad,
timeout: 3600,
}, HIGHLOAD_V3_CODE));Then send batches efficiently:
await wallet.sendBatch(
secretKey,
actions,
subwalletId,
queryHandler,
timeout,
value,
SendMode.PAY_GAS_SEPARATELY,
Date.now() / 1000 - 60
);Frequently Asked Questions (FAQ)
What is the purpose of seqno in TON wallets?
Seqno (sequence number) prevents replay attacks by ensuring each transaction is processed only once. It increments with every sent message and must match the value stored in the contract.
How does signature verification work?
The wallet extracts the signature from the incoming message and compares it against the stored public key using check_signature(). Only valid signatures allow transaction execution.
Why use external messages instead of internal ones?
External messages originate off-chain (e.g., from a user’s app), while internal messages flow between contracts. To initiate actions from outside the blockchain, external messages are required.
Can I send more than four messages at once?
Yes—using high-load wallets. While standard wallets support up to four messages per transaction via cell references, high-load wallets use query-based batching to handle hundreds or thousands of operations.
What is State Init and when is it used?
State Init contains a contract’s initial code and data. It's used during deployment to define what code runs at the new address and how it starts.
How do I avoid common deployment errors?
Always:
- Use Mainnet for testing.
- Ensure sufficient balance (≥0.1 TON recommended).
- Correctly set
subwallet_idandtimeout. - Verify compiled code hashes before deployment.
👉 Explore advanced smart contract patterns used in production dApps.
Final Thoughts
Mastering wallet smart contracts on TON empowers you to build secure, scalable blockchain applications. By understanding message structures, signature validation, and deployment workflows at a low level, you gain full control over your projects.
While high-level libraries simplify development, knowing the underlying mechanics ensures robustness and troubleshooting efficiency. As TON evolves with greater adoption in DeFi, NFTs, and Web3 services, these skills will become increasingly valuable.
Continue exploring through official resources like FunC Overview, Smart Contract Guidelines, and real-world examples to deepen your expertise.
Core Keywords: TON wallet smart contract, The Open Network development, blockchain message handling, seqno replay protection, TON transaction structure, high-load wallet v3, deploy smart contract TON