Payments processing
This page explains how to process (send and accept) digital assets on TON Blockchain. While it primarily focuses on handling TON coins, the theoretical concepts are also relevant for processing jettons
.
It's recommended to review the Asset Processing Overview before reading this tutorial.
Wallet smart contract
Wallet smart contracts on the TON Network allow external actors to interact with blockchain entities. They serve the following purposes:* Authenticates the owner:
- Authenticating the owner: Rejects requests that attempt to process transactions or pay fees on behalf of unauthorized users.
- Providing replay protection: Prevents the repeated execution of the same request, such as sending assets to another smart contract multiple times.
- Initiating arbitrary interactions with other smart contracts.
The standard solution for authentication relies on public-key cryptography. The wallet
stores the public key and verifies that any incoming request is signed by the corresponding private key, which is known only to the owner.
The solution for replay protection varies. Generally, a request contains a fully formed inner message that the wallet
sends to the network. However, different approaches exist for preventing replay attacks.
Seqno-based wallets
Seqno-based wallets use a simple seqno
method to process messages. Each message includes a special seqno integer that must match the counter stored in the wallet smart contract. The wallet
updates this counter with each request, ensuring that no request is processed twice. There are multiple versions of seqno-based wallets, which may differ in publicly available methods. These variations include: the ability to limit requests by expiration time and the ability to operate multiple wallets with the same public key. However, this approach has a limitation: requests must be sent sequentially. Any gap in the seqno
sequence will prevent the processing of all subsequent requests.
High-load wallets
High-load wallets take a different approach by storing the identifiers of non-expired processed requests in the smart contract’s storage. Each new request is checked against previously processed ones, and any detected duplicates are dropped. Since expired requests are removed over time, the contract does not store all requests indefinitely. This method allows multiple requests to be processed in parallel without interference. However, it requires more sophisticated monitoring to track request processing.
Wallet deployment
To deploy a wallet via TonLib, follow these steps:
- Generate a private/public key pair using createNewKey or its wrapper functions (example in tonlib-go). The private key is generated locally and never leaves the host machine.
- Form InitialAccountWallet structure corresponding to one of the available wallet versions. Currently, the supported
wallets
are:wallet.v3
,wallet.v4
,wallet.highload.v1
,wallet.highload.v2
are available. - Calculate the address of the new
wallet
smart contract using the getAccountAddress method. It is recommended to use revision 0 by default. Deploy wallets in the basechainworkchain=0
to minimize processing and storage fees. - Send some Toncoin to the calculated address. The transfer should be made in
non-bounce
mode since the wallet address has no code yet and cannot process incoming messages. Thenon-bounce
flag ensures that if processing fails, the funds are not returned via a bounce message. Warning: Avoid using the non-bounce flag for other transactions, especially when transferring large sums, as the bounce mechanism provides protection against mistakes. - Form the desired action, such as
actionNoop
for deploy only. Then use createQuery and sendQuery to initiate interactions with the blockchain. - Check the contract’s status after a few seconds using the getAccountState method.
Read more in the Wallet Tutorial
Checking wallet address validity
Most SDKs automatically verify a wallet address during creation or transaction preparation, so additional manual validation is usually unnecessary.
- JS (Tonweb)
- tonutils-go
- Ton4j
- ton-kotlin
const TonWeb = require("tonweb")
TonWeb.utils.Address.isValid('...')
package main
import (
"fmt"
"github.com/xssnick/tonutils-go/address"
)
if _, err := address.ParseAddr("EQCD39VS5j...HUn4bpAOg8xqB2N"); err != nil {
return errors.New("invalid address")
}
try {
Address.of("...");
} catch (e) {
// not valid address
}
try {
AddrStd("...")
} catch(e: IllegalArgumentException) {
// not valid address
}
Full Address description on the Smart contract addresses page.
Working with transfers
Check contract's transactions
To retrieve a smart contract's transactions, use the getTransactions. method. This method fetches up to 10 transactions starting from a specified last_transaction_id
and earlier. To process all incoming transactions, follow these steps:
- Obtain the latest
last_transaction_id
using getAddressInformation - Load 10 transactions using the
getTransactions
method. - Process transactions where the source field in the incoming message is not empty and the destination field matches the account address.
- Retrieve the next 10 transactions and repeat steps 2 and 3 until all incoming transactions are processed.
Retrieve incoming/outgoing transactions
It is possible to track message flows during transaction processing. Since the message flow forms a DAG (Directed Acyclic Graph), follow these steps:
- Use the getTransactions to obtain the current transaction.
- Identify incoming transactions by checking out_msg with tryLocateResultTx.
- Identify outgoing transactions by checking in_msg with tryLocateSourceTx.
- JS
import { TonClient, Transaction } from '@ton/ton';
import { getHttpEndpoint } from '@orbs-network/ton-access';
import { CommonMessageInfoInternal } from '@ton/core';
async function findIncomingTransaction(client: TonClient, transaction: Transaction): Promise<Transaction | null> {
const inMessage = transaction.inMessage?.info;
if (inMessage?.type !== 'internal') return null;
return client.tryLocateSourceTx(inMessage.src, inMessage.dest, inMessage.createdLt.toString());
}
async function findOutgoingTransactions(client: TonClient, transaction: Transaction): Promise<Transaction[]> {
const outMessagesInfos = transaction.outMessages.values()
.map(message => message.info)
.filter((info): info is CommonMessageInfoInternal => info.type === 'internal');
return Promise.all(
outMessagesInfos.map((info) => client.tryLocateResultTx(info.src, info.dest, info.createdLt.toString())),
);
}
async function traverseIncomingTransactions(client: TonClient, transaction: Transaction): Promise<void> {
const inTx = await findIncomingTransaction(client, transaction);
// now you can traverse this transaction graph backwards
if (!inTx) return;
await traverseIncomingTransactions(client, inTx);
}
async function traverseOutgoingTransactions(client: TonClient, transaction: Transaction): Promise<void> {
const outTxs = await findOutgoingTransactions(client, transaction);
// do smth with out txs
for (const out of outTxs) {
await traverseOutgoingTransactions(client, out);
}
}
async function main() {
const endpoint = await getHttpEndpoint({ network: 'testnet' });
const client = new TonClient({
endpoint,
apiKey: '[API-KEY]',
});
const transaction: Transaction = ...; // Obtain first transaction to start traversing
await traverseIncomingTransactions(client, transaction);
await traverseOutgoingTransactions(client, transaction);
}
main();
Send payments
Learn the basics of payment processing from the TMA USDT Payments demo
- The service must deploy a
wallet
and keep it funded to prevent contract destruction due to storage fees. Storage fees are typically less than 1 Toncoin per year. - The service should collect the
destination_address
and an optionalcomment
from the user. To avoid duplicate outgoing payments with the same (destination_address
,value
,comment
), either prohibit unfinished payments with identical parameters or schedule payments so that a new payment starts only after the previous one is confirmed. - Create msg.dataText with
comment
as text. - Create msg.message which includes
destination_address
, emptypublic_key
,amount
, andmsg.dataText
. - Create the Action that should contain a set of outgoing messages.
- Use createQuery and sendQuery to submit the payment to the blockchain.
- The service should regularly poll the getTransactions method of the
wallet
contract. By matching confirmed transactions with outgoing payments (destination_address
,value
,comment
) the service can mark payments as finished and retrieve and display the transaction hash and logical time (lt) to the user. - Requests sent to
v3
ofhigh-load
have a 60-second expiration time by default. After expiration, unprocessed requests can be safely resent following steps 3-6.
If the attached value
is too small, the transaction may fail with the error cskip_no_gas
. n this case, Toncoins will be transferred, but no logic will be executed on the recipient's side (the TVM will not launch). Read more here.
Get the transaction ID
It may be confusing that to get more information about a transaction, the user must scan the blockchain via the getTransactions function. It is not possible to get the transaction ID immediately after sending the message, as the transaction must first be confirmed by the blockchain network. To understand the required pipeline, carefully read Send payments, especially the 7th point.
Invoice-based approach
To accept payments based on attached comments, the service should:
- Deploy the
wallet
contract. - Generate a unique
invoice
for each user. A uuid32 string is sufficient for invoice identification. - Users should send payments to the service’s
wallet
contract with theinvoice
as a comment. - Service should regularly poll the getTransactions method for the
wallet
contract. - For each new transaction extract the incoming message, match the comment against stored invoice data, and deposit the Toncoin value into the user's account.
To calculate the incoming message value that a message brings to a contract, we need to analyze the transaction. This happens when a message enters the contract. The transaction can be retrieved using getTransactions. For an incoming wallet transaction, the correct data consists of one incoming message and zero outgoing messages. Otherwise, either an outgoing message is sent to the wallet, in which case the owner spends the Toncoin, or the wallet is not deployed and the incoming transaction is returned.
In any case, in general, the amount a message brings to a contract can be calculated as the incoming message value minus the sum of the outgoing message values minus the fee: value_{in_msg} - SUM(value_{out_msg}) - fee
. Technically, the transaction view contains three different fields with fee
in the name: fee
, storage_fee
and other_fee
, i.e. the total fee, the portion of the fee related to storage costs, and the portion of the fee related to processing the transaction. Only the first one should be used.
Invoices with TON Connect
Best for dApps that require multiple transactions within a session or a persistent wallet connection.
Advantages
- ✅ Permanent communication channel with the wallet.
- ✅ Users only scan a QR code once.
- ✅ Can track transaction confirmation via the returned BOC.
- ✅ Ready-made SDKs and UI kits for various platforms.
Disadvantages
- ❌ If only one payment is needed, users must connect the wallet and confirm the transaction
- ❌ More complex integration than a ton:// link.
Learn more
Invoices with ton:// link
The Ton link is deprecated. Avoid using it.
If you need an easy integration for a simple user flow, it is suitable to use the ton:// link. It is best suited for one-time payments and invoices.
ton://transfer/<destination-address>?
[nft=<nft-address>&]
[fee-amount=<nanocoins>&]
[forward-amount=<nanocoins>]
Advantages
- ✅ Easy integration.
- ✅ No need to connect a wallet.
Disadvantages
- ❌ Users must scan a new QR code for each payment.
- ❌ Cannot track if the user signed the transaction.
- ❌ No information about the user’s address.
- ❌ Requires workarounds for platforms where links are not clickable (e.g., Telegram Desktop bots).
Learn more about ton links here
Explorers
The official TON blockchain explorer: https://tonscan.org.
To generate a transaction link in the explorer, the service needs to get the lt (logical time), the transaction hash, and the account address (the address of the account for which the lt and txhash were obtained using the getTransactions method). Then https://tonscan.org and https://explorer.toncoin.org/ can display the page for this transaction in the following format:
https://tonviewer.com/transaction/{txhash as base64url}
https://tonscan.org/tx/{lt as int}:{txhash as base64url}:{account address}
https://explorer.toncoin.org/transaction?account={account address}<={lt as int}&hash={txhash as base64url}
Note: tonviewer and tonscan also support external-in message hashes instead of transaction hashes for explorer links. This is useful when generating an external message and needing an instant transaction link. Learn more about transactions and messages hashes here
Best practices
Wallet creation
- JS
- Go
- Python
-
toncenter:
-
ton-community/ton:
- xssnick/tonutils-go:
- psylopunk/pythonlib:
- yungwine/pytoniq:
import asyncio
from pytoniq.contract.wallets.wallet import WalletV4R2
from pytoniq.liteclient.balancer import LiteBalancer
async def main():
provider = LiteBalancer.from_mainnet_config(2)
await provider.start_up()
mnemonics, wallet = await WalletV4R2.create(provider)
print(f"{wallet.address=} and {mnemonics=}")
await provider.close_all()
if __name__ == "__main__":
asyncio.run(main())
Wallet creation for different shards
When under heavy load, TON Blockchain may split into shardsto distribute network activity. A shard is similar to a network segment in Web3.
Just as we distribute service infrastructure in the Web2 world to be as close to the end user as possible, in TON we can deploy contracts that will reside in the same shard as the user's wallet or any other contract that interacts with it.
For example, a DApp that collects fees from users for a future airdrop service could prepare separate wallets for each shard to improve the user experience during peak load days. To achieve maximum processing speed, you would need to deploy one collector wallet per shard.
The shard prefix SHARD_INDEX
of a contract is determined by the first 4 bits of its address hash.
To deploy a wallet to a specific shard, you can use logic based on the following code snippet:
import { NetworkProvider, sleep } from '@ton/blueprint';
import { Address, toNano } from "@ton/core";
import {mnemonicNew, mnemonicToPrivateKey} from '@ton/crypto';
import { WalletContractV3R2 } from '@ton/ton';
export async function run(provider?: NetworkProvider) {
if(!process.env.SHARD_INDEX) {
throw new Error("Shard index is not specified");
}
const shardIdx = Number(process.env.SHARD_INDEX);
let testWallet: WalletContractV3R2;
let mnemonic: string[];
do {
mnemonic = await mnemonicNew(24);
const keyPair = await mnemonicToPrivateKey(mnemonic);
testWallet = WalletContractV3R2.create({workchain: 0, publicKey: keyPair.publicKey});
} while(testWallet.address.hash[0] >> 4 !== shardIdx);
console.log("Mnemonic for shard found:", mnemonic);
console.log("Wallet address:",testWallet.address.toRawString());
}
if(require.main === module) {
run();
}
In the case of a wallet contract, subwalletId
can be used instead of a mnemonic, however subwalletId
is not supported by wallet applications.
Once deployment is complete, you can begin processing using the following algorithm:
- User visits the DApp and requests an action.
- The DApp chooses the closest wallet (matching by 4-bit shard prefix).
- The DApp generates a payload for sending fees to the selected wallet.
This way, you can provide the best user experience regardless of the current network load.
Toncoin deposits (get Toncoins)
- JS
- Go
- Python
- xssnick/tonutils-go:
Checking deposits
package main
import (
"context"
"encoding/base64"
"log"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
)
const (
num = 10
)
func main() {
client := liteclient.NewConnectionPool()
err := client.AddConnectionsFromConfigUrl(context.Background(), "https://ton.org/global.config.json")
if err != nil {
panic(err)
}
api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry()
accountAddr := address.MustParseAddr("0QA__NJI1SLHyIaG7lQ6OFpAe9kp85fwPr66YwZwFc0p5wIu")
// we need fresh block info to run get methods
b, err := api.CurrentMasterchainInfo(context.Background())
if err != nil {
log.Fatal(err)
}
// we use WaitForBlock to make sure block is ready,
// it is optional but escapes us from liteserver block not ready errors
res, err := api.WaitForBlock(b.SeqNo).GetAccount(context.Background(), b, accountAddr)
if err != nil {
log.Fatal(err)
}
lastTransactionId := res.LastTxHash
lastTransactionLT := res.LastTxLT
headSeen := false
for {
trxs, err := api.ListTransactions(context.Background(), accountAddr, num, lastTransactionLT, lastTransactionId)
if err != nil {
log.Fatal(err)
}
for i, tx := range trxs {
// should include only first time lastTransactionLT
if !headSeen {
headSeen = true
} else if i == 0 {
continue
}
if tx.IO.In == nil || tx.IO.In.Msg.SenderAddr().IsAddrNone() {
// external message should be omitted
continue
}
if tx.IO.Out != nil {
// no outgoing messages - this is incoming Toncoins
continue
}
// process trx
log.Printf("found in transaction hash %s", base64.StdEncoding.EncodeToString(tx.Hash))
}
if len(trxs) == 0 || (headSeen && len(trxs) == 1) {
break
}
lastTransactionId = trxs[0].Hash
lastTransactionLT = trxs[0].LT
}
}
- yungwine/pytoniq:
import asyncio
from pytoniq_core import Transaction
from pytoniq import LiteClient, Address
MY_ADDRESS = Address("kf8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM_BP")
async def main():
client = LiteClient.from_mainnet_config(ls_i=0, trust_level=2)
await client.connect()
last_block = await client.get_trusted_last_mc_block()
_account, shard_account = await client.raw_get_account_state(MY_ADDRESS, last_block)
assert shard_account
last_trans_lt, last_trans_hash = (
shard_account.last_trans_lt,
shard_account.last_trans_hash,
)
while True:
print(f"Waiting for{last_block=}")
transactions = await client.get_transactions(
MY_ADDRESS, 1024, last_trans_lt, last_trans_hash
)
toncoin_deposits = [tx for tx in transactions if filter_toncoin_deposit(tx)]
print(f"Got {len(transactions)=} with {len(toncoin_deposits)=}")
for deposit_tx in toncoin_deposits:
# Process toncoin deposit transaction
print(deposit_tx.cell.hash.hex())
last_trans_lt = transactions[0].lt
last_trans_hash = transactions[0].cell.hash
def filter_toncoin_deposit(tx: Transaction):
if tx.out_msgs:
return False
if tx.in_msg:
return False
return True
if __name__ == "__main__":
asyncio.run(main())
Toncoin withdrawals (send Toncoins)
- JS
- Go
- Python
-
toncenter:
-
ton-community/ton:
- xssnick/tonutils-go:
- yungwine/pytoniq:
import asyncio
from pytoniq_core import Address
from pytoniq.contract.wallets.wallet import WalletV4R2
from pytoniq.liteclient.balancer import LiteBalancer
MY_MNEMONICS = "one two tree ..."
DESTINATION_WALLET = Address("Destination wallet address")
async def main():
provider = LiteBalancer.from_mainnet_config()
await provider.start_up()
wallet = await WalletV4R2.from_mnemonic(provider, MY_MNEMONICS)
await wallet.transfer(DESTINATION_WALLET, 5)
await provider.close_all()
if __name__ == "__main__":
asyncio.run(main())
Get contract's transactions
- JS
- Go
- Python
- ton-community/ton:
- xssnick/tonutils-go:
- yungwine/pytoniq:
SDKs
A full list of SDKs for various programming languages (JS, Python, Golang, etc.) is available here.