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
.
Wallet smart contract
Wallet smart contracts are contracts on the TON Network which serve the task of allowing actors outside the blockchain to interact with blockchain entities. Generally, it solves three challenges:
- authenticates owner: Rejects to process and pay fees for non-owners' requests.
- replays protection: Prohibits the repetitive execution of one request, for instance sending assets to some other smart contract.
- initiates arbitrary interaction 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 common as well; generally, a request contains a fully formed inner message wallet
sends to the network. However, for replay protection, there are a few different approaches.
Seqno-based wallets
Seqno-based wallets follow the most simple 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
may be sent in parallel without interfering with each other; however, this approach requires more sophisticated monitoring of request processing.
Wallet deployment
To deploy a wallet via TonLib one needs to:
- 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.
- Form InitialAccountWallet structure corresponding to one of the enabled
wallets
. Currentlywallet.v3
,wallet.v4
,wallet.highload.v1
,wallet.highload.v2
are available. - Calculate the address of a new
wallet
smart contract via the getAccountAddress method. We recommend using a default revision0
and also deploying wallets in the basechainworkchain=0
for lower processing and storage fees. - Send some Toncoin to the calculated address. Note that you need to send them in
non-bounce
mode since this address has no code yet and thus 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 thenon-bounce
flag for other transactions, especially when carrying large sums, since the bounce mechanism provides some degree of protection against mistakes. - Form the desired action, for instance
actionNoop
for deploy only. Then use createQuery and sendQuery to initiate interactions with the blockchain. - 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.
- 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.
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:
- The latest
last_transaction_id
can be obtained using getAddressInformation - List of 10 transactions should be loaded via the
getTransactions
method. - Process transactions with not empty source in incoming message and destination equals to account address.
- The next 10 transactions should be loaded and steps 2,3,4,5 should be repeated until you processed all incoming transactions.
Get 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.
- 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
- 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. - Service should get from the user
destination_address
and optionalcomment
. 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. - Form msg.dataText with
comment
as text. - Form msg.message which contains
destination_address
, emptypublic_key
,amount
andmsg.dataText
. - Form Action which contains a set of outgoing messages.
- Use createQuery and sendQuery queries to send outgoing payments.
- 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). - Requests to
v3
ofhigh-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).
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
- Deploy the
wallet
contract. - Generate a unique
invoice
for each user. String representation of uuid32 will be enough. - Users should be instructed to send Toncoin to the service's
wallet
contract with an attachedinvoice
as a comment. - Service should regularly poll the getTransactions method for the
wallet
contract. - 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
Invoices with ton:// link
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 as int}&hash={txhash as base64url}
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())
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:
psylopunk/pythonlib:
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:
- psylopunk/pythonlib:
- yungwine/pytoniq:
SDKs
You can find a list of SDKs for various languages (JS, Python, Golang, C#, Rust, etc.) list here.