Sending TON transactions
Please note: sending a transaction is an irreversible process. It is impossible to cancel the sending after confirmation.
- Transaction preparation:
- The application retrieves the current wallet
seqnovia RPC (fetchSeqno).
Seqno (sequence number) is the sequential number of a wallet transaction in TON. It’s a counter that shows how many transactions this wallet has already sent.
- Gets the latest blockchain block for transaction relevance.
- Forms an internal message with parameters:
- recipient address
- amount in nanotons
- payload (comment or data)
stateInit(if recipient wallet activation is needed)
- Creating external message:
- An external message is created to the wallet smart contract.
- If
seqno === 0(first transaction),initis added for contract deployment. - The message is signed locally with private key (via
WalletContract.createTransfer).
- Signing:
- Private key is extracted from secure storage after authentication (PIN/biometrics).
- Signing is performed locally on the device; private key never leaves the device.
- Network transmission:
- Signed message is serialized to BOC (Bag of Cells).
- Sent to TON RPC node via
client.sendMessage(msg.toBoc()). - Application registers transaction as “pending” for status tracking.
- Confirmation waiting:
- Application tracks status via
BlocksWatcher(SSE connection to blockchain). - When transaction appears in block, status changes from “pending” to “confirmed”.
Sending TON tokens
Sending token is sending an internal message to the sender’s token-wallet address.- Sender in Tonhub signs and sends an external TON transaction to their token-wallet with token transfer instruction.
- This external transaction goes to the sender’s main wallet contract.
- Token-wallet sends internal message to recipient’s token-wallet (or creates it if needed).
- In the payload of message to token-wallet:
op_code=0xf8a7ea5(token Transfer)- token amount
- recipient address
- address for excess return (usually sender’s address)
forward_ton_amount- fee for token delivery to recipientforward_payload- comment, if any
Sending Solana transactions
- Getting latest blockhash:
- Request
getLatestBlockhash()to Solana RPC node. - Blockhash is needed for transaction validity (usually valid ~60 seconds).
- Building transaction:
SystemProgram.transferinstruction is created.
- Adding comment (memo):
- If there’s a comment, instruction is added with
programId= “MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr”.
- Transaction signing:
- Private key is extracted after authentication.
- Transaction is signed locally via
transaction.sign(keyPair). - Signing is performed on device.
- Serialization and sending:
- Transaction is serialized to base64 via
transaction.serialize().toString('base64'). - Sent to Solana RPC via
client.sendEncodedTransaction(encodedTx). - Returns signature (base58), which is the transaction ID.
- Fallback mechanism:
- If main RPC is unavailable, public fallback client is used.
Sending SPL tokens (Solana)
Structure is similar to sending SOL, but with specifics:- Tonhub supports displaying a single token on SOL
- Token Account (ATA):
- Each SPL token is stored in separate token account (Associated Token Account).
- ATA address is computed deterministically from owner address and token mint address.
- Automatic ATA creation:
- When sending token, application checks for recipient’s ATA existence.
- If ATA doesn’t exist,
createAssociatedTokenAccountinstruction is automatically added. - This requires additional SOL fee (~0.002–0.003 SOL).
- Transaction structure:
- Instruction 1 (if needed):
createAssociatedTokenAccountfor recipient. - Instruction 2:
createTransferInstructionto transfer tokens from sender’s ATA to recipient’s ATA.
Transaction monitoring
TON
- BlocksWatcher (SSE):
- Application establishes SSE connection with Tonhub API (
mainnet-v4.tonhubapi.comortestnet-v4.tonhubapi.com). - When new block appears, server sends list of changed addresses.
- If user’s address is in list, application requests new transactions.
- Transaction requests:
- Uses
fetchTransactionsPagewith cursor pagination. - Transactions are loaded in batches (usually 20–50).
- Processing:
- Parsing message bodies to determine operation type (regular transfer, token transfer, etc.).
- Updating balances and transaction history in UI.
Solana
- Polling via RPC:
- Periodic requests to Solana RPC to get transactions by address. No exact update interval - request is made each time when opening/enabling screen.
- Uses
getSignaturesForAddressandgetTransaction.
- Token account monitoring:
- For each known SPL token, corresponding ATA balance is checked.
- Balance changes are interpreted as incoming/outgoing transactions.
- Processing:
- Parsing transaction instructions to determine operation type.
- Updating SOL and SPL token balances.
Transaction amount limits
TON, SOL — no minimum sending amount restrictions. User can send 0, however system will display zero transaction warning. Incoming transactions under 0.1 TON (user-configurable value) are automatically classified as SPAM, and notifications for such operations are not sent. SOL transactions are not marked as spam. Maximum transaction amount is limited by balance size minus fees.Tokens — minimum token transfer amount must exceed zero. Exact minimum value is determined by specific token characteristics (usually one smallest unit, e.g. 0.000001 for USDT). TON fee is paid by sender, except when using gasless transactions.
Transaction Statuses
Pending (awaiting confirmation):- Transaction sent but not yet included in a block.
- For TON: waiting for block inclusion via BlocksWatcher.
- For Solana: waiting for confirmation via RPC polling.
- Transaction included in a block and confirmed by the network.
- For TON: usually 1 confirmation is sufficient.
- For Solana: usually requires multiple confirmations (finality).
- Transaction rejected by the network (insufficient funds, invalid address, etc.).
- Application shows error to user.
Why a user might “not see” an incoming transaction
- Wrong network. TON: testnet is selected in the app, but the transfer came to mainnet (or vice versa). Addresses in mainnet and testnet differ (different format/flag). Need to check the network switch and ensure the sender used the address and network corresponding to the one selected in Tonhub. Solana: mainnet is selected, but the transfer was made in devnet (or vice versa). History and balance are requested separately for mainnet and devnet. Check the network with the sender and in Tonhub (if there’s a switch).
- Wrong asset / wrong screen. TON: incoming token is displayed in the tokens sections, not in the “main” TON history. If the user only looks at the native TON transfer list, they won’t see token deposits. Solana: incoming SPL token is visible in the balance and history of the specific token (by ATA). If the token isn’t added to the list or the user only looks at SOL history, incoming token transfers can be missed.
- RPC lag and update delay. TON: data comes through Tonhub API and BlocksWatcher. During delays or SSE interruptions, transaction list updates may come with delay. Manual refresh (pull-to-refresh) or reopening the screen helps. Solana: transaction list isn’t polled by timer, but updates when opening the screen and when returning to the app. If RPC or indexer lags, new incoming transactions will only appear at the next such update. Recommendation: refresh manually or wait and reopen the screen.
- Address not activated (TON). Transfer came to an address that has never sent a transaction (contract not deployed). Such incoming transfers may not display in history until first wallet activation. User should perform first outgoing transaction (or activation), after which balance and history should update.
- Sender error or rollback. Transaction didn’t make it to a block (rejected by network, insufficient fee from sender, expired blockhash in Solana, etc.). Recipient physically received nothing, so there’s no transaction in their history. Need to check transaction status and hash in explorer with sender (TON / Solana).
Edge cases and retry sending
Transaction sent but RPC didn’t return response
TON and Solana: On RPC timeout, retry with backoff is performed (TON — up to 15 attempts, Solana — up to 5). If all attempts fail, error is shown, pending is not created.Risk: RPC might have accepted the transaction, but response didn’t reach. On retry sending, a second transaction is created — possible double spending.
Recommendation: On long wait and error, check history and balance before retry sending.
Transaction stuck in pending status
Timeout for TON and Solana is 60 seconds. App polls status every ~6 seconds. On confirmation, pending transitions to sent. If no confirmation within 60 seconds, status changes to timed-out without re-debiting funds.Solana: blockhash expiration
In Solana, transactions have limited lifetime bylastValidBlockHeight. Tonhub doesn’t check blockhash expiration separately — transaction remains pending until confirmation or 60-second timeout.
Retry sending policy
TON: “Send again” button appears only after transitioning to timed-out status.Solana: No retry button. After timeout, user creates new transfer manually.
- While transaction is in pending status (first 60 seconds), app only waits for confirmation.
Security when sending
- Contacts. The app implements a contacts section that allows saving familiar addresses and assigning names to them. When sending or receiving transactions, the contact name is automatically displayed to the user, providing confidence in the correct recipient selection.
- Blocked addresses (deny list / spam). Some addresses are automatically marked by the wallet as spam, helping avoid accidental fund transfers to them. The list of such addresses is stored on the backend and can be supplemented manually. Sending to such addresses isn’t completely blocked — only visual indication is implemented to warn the user.
- Restricted addresses. For addresses marked as restricted (e.g., certain staking pools), a dialog with warning message is displayed before sending. Sending is only possible after explicit user confirmation (“Continue anyway”). This feature is related to smart contract limitations, not phishing protection.
- Domains (.ton). When entering a .ton domain, the system performs address resolution and checks correspondence of resolved address to target before sending (including token transactions). If mismatch is detected,
transfer.error.invalidDomainerror is displayed. This reduces risk of address substitution through fake domains. - Network mismatch warning. If user is in mainnet but entered address has testnet format (or vice versa), system displays warning and requests confirmation (
transfer.error.addressIsForTestnet). This helps prevent accidental fund transfers to wrong network.
Gas when sending
Transaction fee is always paid by the sender, regardless of the type of assets being transferred. Tonhub doesn’t provide users the ability to adjust fee size. Fee amount is calculated with margin to guarantee coverage of all transaction execution costs. Unused portion of the fee is automatically returned to user’s balance as separate incoming transaction. Fee payment is made exclusively in the native currency of the corresponding blockchain: TON for TON network and SOL for Solana network. This feature can lead to situations where user has tokens in account but cannot send them due to lack of native currency for fee payment. To solve this problem, a new wallet version W5 was developed in the TON ecosystem, which supports gasless transaction functionality.Gasless transactions
Gasless transaction functionality doesn’t imply complete absence of fees for the user. This technology allows paying fees when sending certain tokens in the currency of the tokens themselves. Tonhub supports gasless transactions exclusively for TON blockchain. For example, when using W5 wallet, user can pay fee for sending USDT directly with USDT tokens, without having TON in balance. User can choose fee payment method: in token currency (amount will be higher due to exchange costs) or in native currency.How gasless works
Gasless sending is essentially an overlay over regular transactions. The network cannot send transactions without paying fees in native currency, since these funds go to pay validators’ work. In gasless sending, the relayer pays the transaction fee, taking part of the sent token for itself. Relayer in gasless context is a service with TON wallet that:- Accepts signed transaction from user (user pays fee in token)
- Independently sends this transaction to blockchain and pays fee in TON from its wallet
- Retains fee in token
Gasless limitations
- Available only for part of tokens; list comes from server (config); example — USDT.
- Supported only on V5 wallets
- Cooldown exists (paying fee in token once every few minutes); If needed, user can wait or pay in TON.