跳到主要内容

Payments Processing

This page explains how to process (send and accept) digital assets on the TON blockchain. It mostly describes how to work with TON coins, but theoretical part is important even if you want to process only jettons.

提示

It's recommended to get acquainted with 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.

  • Authenticates the owner: Rejects requests that attempt to process or pay fees on behalf of non-owners.
  • Provides replay protection: Prevents the repeated execution of the same request, such as sending assets to another smart contract.
  • Initiates arbitrary interactions with other smart contracts.

Standard solution for the first challenge is public-key cryptography: wallet stores the public key and checks that an incoming message with a request is signed by the corresponding private key which is known only by the owner.

The solution to the third challenge is also common; generally, a request contains a fully formed inner message that the wallet sends to the network. However, for replay protection, there are a few different approaches.

Seqno-based wallets

Seqno-based wallets use the simplest approach to sequencing messages. Each message has a special seqno integer that must coincide with the counter stored in the wallet smart contract. wallet updates its counter on each request, thus ensuring that one request will not be processed twice. There are a few wallet versions that differ in publicly available methods: the ability to limit requests by expiration time, and the ability to have multiple wallets with the same public key. However, an inherent requirement of that approach is to send requests one by one, since any gap in seqno sequence will result in the inability to process all subsequent requests.

High-load wallets

This wallet type follows an approach based on storing the identifier of the non-expired processed requests in smart-contract storage. In this approach, any request is checked for being a duplicate of an already processed request and, if a replay is detected, dropped. Due to expiration, the contract may not store all requests forever, but it will remove those that cannot be processed due to the expiration limit. Requests to this wallet can be sent in parallel without interference, but this approach requires more sophisticated monitoring of request processing.

Wallet deployment

To deploy a wallet via TonLib, one needs to:

  1. Generate a private/public key pair via createNewKey or its wrapper functions (example in tonlib-go). Note that the private key is generated locally and does not leave the host machine.
  2. Form InitialAccountWallet structure corresponding to one of the enabled wallets. Currently wallet.v3, wallet.v4, wallet.highload.v1, wallet.highload.v2 are available.
  3. Calculate the address of a new wallet smart contract via the getAccountAddress method. We recommend using a default revision 0 and also deploying wallets in the basechain workchain=0 for lower processing and storage fees.
  4. Send some Toncoin to the calculated address. Note that you need to send them in non-bounce mode, as this address has no code yet and cannot process incoming messages. non-bounce flag indicates that even if processing fails, money should not be returned with a bounce message. We do not recommend using the non-bounce flag for other transactions, especially when carrying large sums, since the bounce mechanism provides some degree of protection against mistakes.
  5. Form the desired action, for instance actionNoop for deploy only. Then use createQuery and sendQuery to initiate interactions with the blockchain.
  6. Check the contract in a few seconds with getAccountState method.
提示

Read more in the Wallet Tutorial

Check the validity of wallet address

Most SDKs force you to verify address (most verify it during wallet creation or transaction preparation process), so, usually, it's not require any additional complex steps from you.

  const TonWeb = require("tonweb")
TonWeb.utils.Address.isValid('...')
提示

Full Address description on the Smart Contract Addresses page.

Work with transfers

Check contract's transactions

A contract's transactions can be obtained using getTransactions. This method allows to get 10 transactions from some last_transaction_id and earlier. To process all incoming transactions, the following steps should be followed:

  1. The latest last_transaction_id can be obtained using getAddressInformation
  2. List of 10 transactions should be loaded via the getTransactions method.
  3. Process transactions with not empty source in incoming message and destination equals to account address.
  4. The next 10 transactions should be loaded and steps 2,3,4,5 should be repeated until you processed all incoming transactions.

Retrieve Incoming/Outgoing Transactions

It's possible to track messages flow during transaction processing. Since the message flow is a DAG it's enough to get current transaction using getTransactions method and find incoming transaction by out_msg with tryLocateResultTx or outgoing transactions by in_msg with tryLocateSourceTx.

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 on basic example of payments processing from TMA USDT Payments demo

  1. Service should deploy a wallet and keep it funded to prevent contract destruction due to storage fees. Note that storage fees are generally less than 1 Toncoin per year.
  2. Service should get from the user destination_address and optional comment. Note that for the meantime, we recommend either prohibiting unfinished outgoing payments with the same (destination_address, value, comment) set or proper scheduling of those payments; that way, the next payment is initiated only after the previous one is confirmed.
  3. Form msg.dataText with comment as text.
  4. Form msg.message which contains destination_address, empty public_key, amount and msg.dataText.
  5. Form Action which contains a set of outgoing messages.
  6. Use createQuery and sendQuery queries to send outgoing payments.
  7. Service should regularly poll the getTransactions method for the wallet contract. Matching confirmed transactions with the outgoing payments by (destination_address, value, comment) allows to mark payments as finished; detect and show the user the corresponding transaction hash and lt (logical time).
  8. Requests to v3 of high-load wallets have an expiration time equal to 60 seconds by default. After that time unprocessed requests can be safely resent to the network (see steps 3-6).
警告

If value attached is too small transaction can get aborted with error cskip_no_gas. In this case Toncoins will be transferred successfully but no logic on other side will be executed (TVM won't even launch). About gas limits you can read more here.

Get transaction id

It can be unclear that to get more information on transaction user must scan blockchain through getTransactions function. It is impossible to retrieve the transaction ID immediately after sending a message, as the transaction must first be confirmed by the blockchain network. To understand required pipeline read Send payments carefully, especially 7th point.

Invoice-based approach

To accept payments based on attached comments, the service should

  1. Deploy the wallet contract.
  2. Generate a unique invoice for each user. String representation of uuid32 will be enough.
  3. Users should be instructed to send Toncoin to the service's wallet contract with an attached invoice as a comment.
  4. Service should regularly poll the getTransactions method for the wallet contract.
  5. For new transactions, the incoming message should be extracted, comment matched against the database, and the incoming message value deposited to the user's account.

To calculate the incoming message value that the message brings to the contract, one needs to parse the transaction. It happens when the message hits the contract. A transaction can be obtained using getTransactions. For an incoming wallet transaction, the correct data consists of one incoming message and zero outgoing messages. Otherwise, either an external message is sent to the wallet, in which case the owner spends Toncoin, or the wallet is not deployed and the incoming transaction bounces back.

Anyway, in general, the amount that a message brings to the contract can be calculated as the value of the incoming message minus the sum of the values of the outgoing messages minus the fee: value_{in_msg} - SUM(value_{out_msg}) - fee. Technically, transaction representation contains three different fields with fee in name: fee, storage_fee, and other_fee, that is, a total fee, a part of the fee related to storage costs, and a part of the fee related to transaction processing. Only the first one should be used.

Invoices with TON Connect

Best suited for dApps that need to sign multiple payments/transactions within a session or need to maintain a connection to the wallet for some time.

  • ✅ There's a permanent communication channel with the wallet, information about the user's address

  • ✅ Users only need to scan a QR code once

  • ✅ It's possible to find out whether the user confirmed the transaction in the wallet, track the transaction by the returned BOC

  • ✅ Ready-made SDKs and UI kits are available for different platforms

  • ❌ If you only need to send one payment, the user needs to take two actions: connect the wallet and confirm the transaction

  • ❌ Integration is more complex than the ton:// link

Learn More

warning

Ton link is deprecated, avoid using this

If you need an easy integration for a simple user flow, it is suitable to use the ton:// link. Best suited for one-time payments and invoices.

ton://transfer/<destination-address>?
[nft=<nft-address>&]
[fee-amount=<nanocoins>&]
[forward-amount=<nanocoins>]
  • ✅ Easy integration

  • ✅ No need to connect a wallet

  • ❌ Users need to scan a new QR code for each payment

  • ❌ It's not possible to track whether the user has signed the transaction or not

  • ❌ No information about the user's address

  • ❌ Workarounds are needed on platforms where such links are not clickable (e.g. messages from bots for Telegram desktop clients )

Learn more about ton links here

Explorers

The blockchain explorer is https://tonscan.org.

To generate a transaction link in the explorer, the service needs to get the lt (logic time), transaction hash, and account address (account address for which lt and txhash were retrieved via the getTransactions method). https://tonscan.org and https://explorer.toncoin.org/ may then show the page for that tx 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={lt as int}&hash={txhash as base64url}

Note that tonviewer and tonscan supports external-in msg hash instead of transaction hash for link in explorer. That can become useful when you generate external message and want instant link generation. More about transactions and messages hashes here

Best Practices

Wallet Creation

Wallet Creation for Different Shards

When under heavy load, the TON blockchain may split into shards. A simple analogy for a shard in the Web3 world would be a network segment.

Just as we distribute service infrastructure in the Web2 world to be as close as possible to the end user, in TON, we can deploy contracts to be in the same shard as the user's wallet or any other contract that interacts with it.

For instance, a DApp that collects fees from users for a future airdrop service might prepare separate wallets for each shard to enhance the user experience on peak load days. To achieve the highest processing speed, you will need to deploy one collector wallet per shard.

Shard prefix SHARD_INDEX of a contract is defined by the first 4 bits of it's address hash. In order to deploy wallet into specific shard, one may 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 case of wallet contract, one may use subwalletId instead of mnemonic, however subwalletId is not supported by wallet applications.

Once deployment have completed, you can process with the following algorithm:

  1. User arrives at DApp page and requests action.
  2. DApp picks the closest wallet to the user(matching by 4 bit prefix)
  3. DApp provides user payload sending his fee to the picked wallet.

That way you will be able to provide the best possible user experience regardless current network load.

Toncoin Deposits (Get toncoins)

Toncoin Withdrawals (Send toncoins)

Get contract's transactions

SDKs

A full list of SDKs for various programming languages (JS, Python, Golang, etc.) is available here.