Transfer Jetton Tokens
Transfer Jetton tokens gaslessly with fees paid in paymaster tokens.
This guide explains how to transfer Jetton tokens gaslessly, override paymaster configuration, estimate transfer fees, and run preflight checks.
Both transfer() and quoteTransfer() require globalThis.crypto.getRandomValues() to create a random 64-bit Jetton query ID. Check that your runtime exposes Web Crypto before either call.
Transfer Tokens (Gasless)
The sending account needs deployed Jetton wallets for both the transferred token and the selected paymaster token. Quotes and transfers check each wallet's code, owner and Jetton master; an undeployed wallet or a failed check stops the operation.
You can send Jetton tokens gaslessly using account.transfer(). Fees are deducted from the configured paymaster token:
const result = await account.transfer({
token: 'EQ...', // Jetton master contract address
recipient: 'EQ...', // Recipient's TON address
amount: 1000000000 // Amount in Jetton's base units
})
console.log('Signed transfer body hash:', result.hash)
console.log('Transfer fee:', result.fee, 'paymaster token units')The signed wallet body uses a 120-second timeout, including the first transfer at sequence number 0. The relay estimate's expiry is checked separately; it does not set the signed body's expiry.
Override Paymaster Configuration
The second argument to account.transfer() overrides individual fields. An omitted or undefined paymasterToken or transferMaxFee retains its wallet-level value. For example, override only the fee cap while keeping the configured paymaster:
const result = await account.transfer({
token: 'EQ...',
recipient: 'EQ...',
amount: 1000000000
}, {
transferMaxFee: 2000000000 // Override maximum allowed fee
})
console.log('Signed transfer body hash:', result.hash)
console.log('Transfer fee:', result.fee, 'paymaster token units')transferMaxFee rejects estimates greater than the configured cap. A fee estimate equal to the cap is allowed.
Passing only { paymasterToken } retains the wallet's numeric transferMaxFee; it does not convert the cap to the new token's units. Set an explicit cap in the selected paymaster Jetton's base units when changing tokens.
Before signing or relaying, the wallet verifies that the estimate preserves the requested Jetton transfer and contains only the expected paymaster commission message. The validation checks the paymaster token's deployed Jetton wallet through the configured TON client. Treat validation or provider errors as a failed preflight; do not reconstruct and submit the relay messages yourself.
Estimate Transfer Fees
You can get a fee estimate before executing the transfer using account.quoteTransfer():
const quote = await account.quoteTransfer({
token: 'EQ...',
recipient: 'EQ...',
amount: 1000000
})
console.log('Transfer fee estimate:', quote.fee, 'paymaster token units')quoteTransfer() performs the same estimate validation and deployed-wallet checks as transfer() before returning the commission. An omitted or undefined per-call paymaster uses the wallet-level token. The quote does not enforce transferMaxFee.
Preflight Transfer Checks
Inspect balances and fees before transferring:
- Use
account.getTokenBalance()to check Jetton balance. - Use
account.quoteTransfer()with the intended paymaster to estimate the fee. - Query that paymaster Jetton explicitly with
getTokenBalance(paymasterJettonAddress).getPaymasterTokenBalance()always reads the wallet-level paymaster, so do not use it to preflight a per-call override. - If the transferred Jetton is also the paymaster Jetton, require one balance to cover the transfer amount plus the fee. Compare parsed TON addresses because different string encodings can identify the same Jetton master.
- Reject a quote above your application's fee policy, then execute
account.transfer()withtransferMaxFeeset to that quote. The method obtains a fresh estimate and aborts before relay if it has risen. This explicit cap bounds the new estimate even when the wallet-level cap allows a higher fee:
This example imports Address from @ton/ton for canonical address comparison. Add @ton/ton as a direct dependency in your application before using it.
import { Address } from '@ton/ton'
async function transferWithChecks(account, jettonAddress, paymasterJettonAddress, recipient, amount, maxFee) {
if (typeof jettonAddress !== 'string' || jettonAddress.length === 0) {
throw new Error('Invalid Jetton address format')
}
if (typeof paymasterJettonAddress !== 'string' || paymasterJettonAddress.length === 0) {
throw new Error('Invalid paymaster Jetton address format')
}
if (typeof recipient !== 'string' || recipient.length === 0) {
throw new Error('Invalid recipient address format')
}
const amountBaseUnits = BigInt(amount)
const maxFeeBaseUnits = BigInt(maxFee)
const transferOptions = {
token: jettonAddress,
recipient,
amount: amountBaseUnits
}
const paymasterToken = { address: paymasterJettonAddress }
const transferBalance = await account.getTokenBalance(jettonAddress)
if (transferBalance < amountBaseUnits) {
throw new Error('Insufficient Jetton balance')
}
const quote = await account.quoteTransfer(transferOptions, { paymasterToken })
console.log('Estimated fee (paymaster token):', quote.fee)
if (quote.fee > maxFeeBaseUnits) {
throw new Error('Quoted fee exceeds application policy')
}
const paymasterBalance = await account.getTokenBalance(paymasterJettonAddress)
const sameJetton = Address.parse(jettonAddress).equals(Address.parse(paymasterJettonAddress))
const requiredPaymasterBalance = sameJetton
? amountBaseUnits + quote.fee
: quote.fee
if (paymasterBalance < requiredPaymasterBalance) {
throw new Error('Insufficient paymaster Jetton balance')
}
const result = await account.transfer(transferOptions, {
paymasterToken,
transferMaxFee: quote.fee
})
console.log('Signed transfer body hash:', result.hash)
console.log('Actual fee (paymaster token):', result.fee)
return result
}Next Steps
Learn how to sign and verify messages with your gasless TON account.