WDK logoWDK documentation
TONGasless TONGuides

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.

Transfer Tokens (Gasless)

You can send Jetton tokens gaslessly using account.transfer(). Fees are deducted from the configured paymaster token:

Gasless Jetton Transfer
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')

Override Paymaster Configuration

You can override the default paymaster token and maximum fee on a per-transfer basis by passing a second configuration argument to account.transfer():

Transfer with Config Override
const result = await account.transfer({
  token: 'EQ...',
  recipient: 'EQ...',
  amount: 1000000000
}, {
  paymasterToken: {
    address: 'EQ...' // Override default paymaster token
  },
  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.

Estimate Transfer Fees

You can get a fee estimate before executing the transfer using account.quoteTransfer():

Quote Gasless Transfer
const quote = await account.quoteTransfer({
  token: 'EQ...',
  recipient: 'EQ...',
  amount: 1000000
})
console.log('Transfer fee estimate:', quote.fee, 'paymaster token units')

Preflight Transfer Checks

Inspect balances and fees before transferring:

  1. Use account.getTokenBalance() to check Jetton balance.
  2. Use account.quoteTransfer() with the intended paymaster to estimate the fee.
  3. 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.
  4. 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.
  5. Reject a quote above your application's fee policy, then execute account.transfer() with transferMaxFee set to that quote. The method obtains a fresh estimate and aborts before relay if it has risen. This explicit policy check matters because a per-call configuration replaces, rather than merges with, the wallet-level transfer configuration:

This example imports Address from @ton/ton for canonical address comparison. Add @ton/ton as a direct dependency in your application before using it.

Gasless Transfer with Preflight Checks
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.

On this page