This tutorial shows you how to send a trust line token from one holder to another on the XRP Ledger.
Each account must create a trust line to the issuer before it can hold the token. Holders can send the token to each other through the issuer only if the issuer allows rippling on both of their trust lines, either on all of its trust lines by enabling the Default Ripple flag (recommended), or on individual trust lines. This indirect movement of funds is called rippling.
By the end of this tutorial, you will be able to:
- Check that a receiving account can accept a trust line token payment.
- Send a trust line token payment between two accounts.
- Verify the payment was successful.
To complete this tutorial, you should:
- Have a basic understanding of the XRP Ledger.
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
- JavaScript with the xrpl.js library. See Get Started Using JavaScript for setup steps.
- Python with the xrpl-py library. See Get Started Using Python for setup steps.
- Go with the xrpl-go library. See Get Started Using Go for setup steps.
You can find the complete source code for this tutorial's examples in the code samples section of this website's repository.
From the code sample folder, use npm to install dependencies.
npm installTo get started, import the necessary libraries and instantiate a client to connect to the XRPL. This example imports:
xrpl: Used for XRPL client connection, transaction submission, and wallet handling.bignumber.js: Used to compare token amounts without floating-point rounding.fs: Used to check for and load the tutorial setup data../sendTrustLineTokenSetup.js: The tutorial set up script, imported and called directly.
import xrpl from 'xrpl'
import BigNumber from 'bignumber.js'
import fs from 'fs'
import { setup } from './sendTrustLineTokenSetup.js'
// Set up client ----------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
await client.connect()Next, load the sender wallet, and set the token to send and the receiver address. This example uses pre-configured data from the setup script, which funds the sender and buys TST for it on the Decentralized Exchange (DEX), but you can replace these with your own values.
// This step checks for the necessary setup data to run the tutorial.
// If missing, sendTrustLineTokenSetup.js will generate it.
if (!fs.existsSync('sendTrustLineTokenSetup.json')) {
console.log(`\n=== Tutorial setup data doesn't exist. Running setup script... ===\n`)
await setup()
}
// Load the preconfigured sender account.
const setupData = JSON.parse(fs.readFileSync('sendTrustLineTokenSetup.json', 'utf8'))
const sender = xrpl.Wallet.fromSeed(setupData.sender.seed)
// The token to send and who to send it to. TST is a test token on Testnet,
// and the receiver is a well-known account that already holds a TST trust
// line. You can replace these values with your own.
const issuerAddress = 'rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd'
const currencyCode = 'TST'
const receiverAddress = 'rG6ZCtKCURU58DP2iiff4qziApBGSuvPqE'
const sendQuantity = '2'
// The highest transfer fee this script is willing to pay, as a percentage of
// the amount sent. Protects against approving a large fee by accident.
const maxTransferFeePercent = 10
console.log(`Issuer address: ${issuerAddress}`)
console.log(`Sender address: ${sender.address}`)
console.log(`Receiver address: ${receiverAddress}`)
// getTrustLine returns the trust line between `account` and `peer` for the
// given currency, or undefined if no such trust line exists.
async function getTrustLine(account, currency, peer) {
const pages = await client.requestAll({
command: 'account_lines',
account,
peer,
ledger_index: 'validated'
})
return pages.flatMap(page => page.result.lines).find(line => line.currency === currency)
}TST is a test token available on Testnet, and the receiver is an account that already holds a TST trust line.
The getTrustLine helper uses the account_lines method to look up the trust line between an account and the issuer, and the remaining steps use it to read balances. The response is paginated, and an account that owns many ledger entries can return a first page with no matching lines and a marker, so the helper follows the markers until it finds the line.
Before sending the payment, check the sender's balance using the account_lines method. The sender should have an initial balance from the setup script, and at least enough to cover the payment plus any transfer fee the issuer charges.
// Check sender balance ----------------------
// Confirm the sender holds enough of the token to make the payment, including
// the issuer's transfer fee, which the sender pays on top of the amount sent.
console.log(`\n=== Checking sender's ${currencyCode} balance... ===\n`)
const issuerInfo = await client.request({
command: 'account_info',
account: issuerAddress,
ledger_index: 'validated'
})
// parseAccountRootFlags turns the Flags bitmask into an object of booleans.
const issuerFlags = xrpl.parseAccountRootFlags(issuerInfo.result.account_data.Flags)
// If the issuer charges a transfer fee, the sender pays more than the
// receiver gets. Work out the SendMax needed and refuse unreasonable fees.
// TransferRate is a multiplier in billionths, e.g. 1005000000 = 1.005 = 0.5% fee.
const transferRate = issuerInfo.result.account_data.TransferRate ?? 1000000000
const transferFee = new BigNumber(xrpl.transferRateToDecimal(transferRate))
const transferFeePercent = transferFee.times(100)
if (transferFeePercent.gt(maxTransferFeePercent)) {
console.error(`Error: Transfer fee of ${transferFeePercent}% exceeds the ${maxTransferFeePercent}% limit.`)
await client.disconnect()
process.exit(1)
}
let sendMax
if (transferFeePercent.gt(0)) {
// Token amounts have at most 16 significant digits. Round up so SendMax
// never falls short of the exact cost.
sendMax = new BigNumber(sendQuantity).times(transferFee.plus(1)).precision(16, BigNumber.ROUND_UP).toString()
console.log(`✓ Transfer fee is ${transferFeePercent}%; sender will pay up to ${sendMax} ${currencyCode}`)
} else {
console.log('✓ No transfer fee')
}
const senderLine = await getTrustLine(sender.address, currencyCode, issuerAddress)
if (!senderLine) {
console.error(`Error: Sender has no ${currencyCode} trust line with the issuer.`)
await client.disconnect()
process.exit(1)
}
console.log(`Sender's balance: ${senderLine.balance}`)
if (new BigNumber(senderLine.balance).lt(sendMax ?? sendQuantity)) {
console.error(`Error: Sender's balance is less than the ${sendMax ?? sendQuantity} ${currencyCode} to send.`)
await client.disconnect()
process.exit(1)
}A trust line is a bidirectional relationship with a single, shared balance that is positive from one account's perspective and negative from the other's. Querying account_lines on a holder with peer set to the issuer returns the positive value.
A payment between two holders can fail for reasons that have nothing to do with the sender. Check the receiver's trust line and the issuer's settings before submitting, so you can tell the user why a payment can't go through instead of letting it fail on the ledger.
First, request the receiver's trust line with account_lines.
// Check receiver ----------------------
// Confirm the receiver can accept the payment before sending it.
console.log(`\n=== Checking receiver can receive ${currencyCode}... ===\n`)
const receiverLine = await getTrustLine(receiverAddress, currencyCode, issuerAddress)Then, run the following checks:
// 1. The receiver must have a trust line to the issuer for this currency.
if (!receiverLine) {
console.error(`Error: Receiver has no ${currencyCode} trust line with the issuer.`)
await client.disconnect()
process.exit(1)
}
console.log('✓ Receiver has a trust line to the issuer')
// 2. The issuer must allow rippling on at least one of the two trust lines, since
// a payment between holders moves through the issuer. Default Ripple on the
// issuer is the usual way to allow this, but check the lines themselves.
if (senderLine.no_ripple_peer && receiverLine.no_ripple_peer) {
console.error('Error: The issuer has disabled rippling on both the sender and receiver trust lines.')
await client.disconnect()
process.exit(1)
}
const defaultRipple = issuerFlags.lsfDefaultRipple === true
console.log(`✓ Rippling is allowed (issuer Default Ripple: ${defaultRipple})`)
// 3. The receiver's limit must cover their current balance plus the payment.
if (new BigNumber(receiverLine.balance).plus(sendQuantity).gt(receiverLine.limit)) {
console.error(`Error: Payment would exceed the receiver's limit of ${receiverLine.limit} ${currencyCode}.`)
await client.disconnect()
process.exit(1)
}
console.log(`✓ Receiver's limit covers the payment`)
// 4. If the issuer requires authorization, the receiver's line must be authorized.
const requireAuth = issuerFlags.lsfRequireAuth === true
if (requireAuth && !receiverLine.peer_authorized) {
console.error('Error: The issuer requires authorization and has not authorized the receiver.')
await client.disconnect()
process.exit(1)
}
console.log(`✓ Authorization OK (issuer Require Auth: ${requireAuth})`)
console.log(`\nBalances before payment:`)
console.log(` Sender: ${senderLine.balance} ${currencyCode}`)
console.log(` Receiver: ${receiverLine.balance} ${currencyCode}`)- Trust line exists: The receiver has a trust line to the issuer for this currency.
- Rippling is allowed: The issuer hasn't set
no_ripple_peeron both the sender's and receiver's lines. - Limit covers the payment: The receiver's
limitis at least theirbalanceplus the amount. - Authorization: If the issuer requires auth, the receiver's line has
peer_authorizedset.
Once the checks pass, the sender can send the token to the receiver with a Payment transaction. Provide the issued currency amount object that specifies the currency code, the issuer's address, and the value to send.
// Send payment ----------------------
// The sender pays the receiver with the token. Amount is what the receiver
// gets; SendMax (if set) is the most the sender is willing to spend.
console.log(`\n=== Sending ${sendQuantity} ${currencyCode} to receiver... ===\n`)
const paymentTx = {
TransactionType: 'Payment',
Account: sender.address,
Destination: receiverAddress,
Amount: {
currency: currencyCode,
issuer: issuerAddress,
value: sendQuantity
}
}
if (sendMax) {
paymentTx.SendMax = {
currency: currencyCode,
issuer: issuerAddress,
value: sendMax
}
}
xrpl.validate(paymentTx)
console.log(JSON.stringify(paymentTx, null, 2))
const paymentResponse = await client.submitAndWait(paymentTx, {
wallet: sender,
autofill: true
})
if (paymentResponse.result.meta?.TransactionResult !== 'tesSUCCESS') {
const resultCode = paymentResponse.result.meta?.TransactionResult
console.error('Error: Unable to send token:', resultCode)
await client.disconnect()
process.exit(1)
}
console.log('Payment successful!')
console.log(`Explorer link: https://testnet.xrpl.org/transactions/${paymentResponse.result.hash}`)The TST issuer doesn't charge a transfer fee, so the sender's balance decreases by the same amount the receiver gains. If you replace the issuer with one that has a non-zero TransferRate, the sender's balance decreases by more than the receiver gains. The SendMax field set in step 3 covers the difference.
If the payment fails, it could be for one of the following reasons:
tecPATH_PARTIAL: the sender doesn't have enough balance to cover the payment. This can also occur if the issuer has configured a non-zeroTransferRateand the sender doesn't have enough to cover the payment plus the transfer fee.tecPATH_DRY: no path could deliver any amount at all. This commonly happens when the receiver has no trust line for the token, the issuer doesn't allow rippling, or the receiver's trust line isn't authorized.
Check the account balances again to confirm the transfer.
// Verify balances ----------------------
// Check both balances again to confirm the transfer.
console.log(`\n=== Checking final ${currencyCode} balances... ===\n`)
// The metadata's delivered_amount is the amount the receiver actually got.
// Always read this field rather than the transaction's Amount, because a
// partial payment can succeed while delivering less than Amount.
const deliveredAmount = paymentResponse.result.meta.delivered_amount
console.log(` Delivered amount: ${deliveredAmount.value} ${deliveredAmount.currency}`)
const senderLineAfter = await getTrustLine(sender.address, currencyCode, issuerAddress)
const receiverLineAfter = await getTrustLine(receiverAddress, currencyCode, issuerAddress)
console.log(` Sender: ${senderLineAfter.balance} ${currencyCode}`)
console.log(` Receiver: ${receiverLineAfter.balance} ${currencyCode}`)
await client.disconnect()The code first prints the delivered amount from the transaction metadata, which is the amount the receiver actually got. Always read this field when processing incoming payments, because a partial payment can return tesSUCCESS while delivering less than Amount.
If either trust line has a non-default Quality setting, that can also affect the amounts each side sees.
The sender's balance should have decreased by the amount sent plus the transfer fee, while the receiver's balance should have increased by only the delivered amount.