Skip to main content

Verify Trustlines

When performing payments on Stellar for Stellar Assets other than XLM, it is important to ensure that the receiving account has a trustline established for the asset being sent. This one-pager provides a quick overview of how to verify trustlines before sending transactions, ensuring that payments are processed smoothly and allowing for application to appropriately handle cases where trustlines are not established or invalid.

Why Verify Trustlines?

In Stellar, trustlines are used to establish a relationship between an account and a Stellar Asset. They are created with the change trust operation, and indicate that the account is willing to hold and transact with that asset. If a trustline is not established for an asset, the account cannot receive payments in that asset, leading to transaction failures.

Furthermore, asset issuers may enforce specific requirements through trustlines, such as maximum balances an account can hold or granular authorization to receive/send the asset or to maintain liabilities. See the Asset Design Considerations for more details on how control flags and trustlines can be used to customize these behaviors.

Verifying trustlines before sending transactions helps ensure that the receiving account meets the requirements and can successfully receive the asset. This allows for the application to handle cases where trustlines are not established or invalid, providing clear feedback and a smooth user experience while preventing failed transactions.

Checking a Trustline through the Stellar RPC

Unlike Horizon, Stellar RPC does not return an account's trustlines alongside the account itself — a trustline is its own ledger entry, so you look it up directly. The JavaScript SDK gives you two ways to do that:

  • getAssetBalance is the quick path. One call tells you whether the trustline exists at all, whether it is authorized, and how much of the asset the account currently holds.
  • getLedgerEntries gives you the raw trustline entry, which you need when you also care about the trustline's limit — the maximum balance the account is willing to hold.

Most applications only need the first. Reach for the second when a large payment could push the receiver past its limit.

Existence and authorization

The following snippet checks that the destination account exists, that it trusts the asset, and that the trustline is authorized to receive it:

import { Asset } from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

// Initialize Stellar RPC server for testnet
const rpc = new Server("https://soroban-testnet.stellar.org");

// The account you're about to pay. Replace this with your real destination.
// On Testnet you can create one with `rpc.requestAirdrop()` and give it a
// trustline with a `changeTrust` operation.
const receiver = "G...";

// First, check to make sure that the destination account exists.
try {
await rpc.getAccount(receiver);
} catch (error) {
console.error("Error checking destination account:", error);
throw error;
}

// Now we define which asset we want to check the trustline for.
// In this case, we are checking for USDC issued on testnet.
const USDC = new Asset(
"USDC",
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
);

// `getAssetBalance` looks up the trustline ledger entry for us and returns it
// already decoded. If the account has no trustline for the asset at all, the
// call throws, so we handle that case here. Note that the SDK reports any
// failure of this lookup — including a network or RPC error — with the same
// "not found" error, so treat this branch as "could not confirm a trustline"
// rather than proof that none exists.
let balanceEntry;
try {
({ balanceEntry } = await rpc.getAssetBalance(receiver, USDC));
} catch (error) {
console.error(
`Could not confirm a trustline for asset ${USDC.code} issued by ${USDC.issuer} for account ${receiver}.`,
);
throw error;
}

// The trustline exists, but the issuer may not have authorized it yet. An
// unauthorized trustline cannot receive payments, so sending would fail.
if (!balanceEntry.authorized) {
console.error(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} is not authorized for account ${receiver}.`,
);
throw new Error("Trustline not authorized");
}

// `amount` is the account's current balance of the asset, as a string of
// stroops (1 unit = 10,000,000 stroops). Keep it as a string or a BigInt
// rather than a Number — see the note on precision below.
console.log(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} is valid for account ${receiver} (${balanceEntry.amount} stroops).`,
);

// Proceed with sending the payment...
tip

getAssetBalance also accepts a contract address, so the same call verifies a Soroban contract's balance of the asset through its Stellar Asset Contract.

warning

getAssetBalance raises the same Trustline for CODE:ISSUER not found for ACCOUNT error whether the trustline is genuinely absent or the lookup simply failed — a timeout or an unreachable RPC endpoint surfaces as "not found" too. If your application needs to tell those cases apart, use getLedgerEntries instead: it returns an empty entries array for a missing trustline and only rejects on a transport error.

Checking the trustline limit

A trustline also carries a limit: the maximum balance of the asset the account is willing to hold. A payment that would push the balance past that limit fails, even when the trustline exists and is authorized.

getAssetBalance does not surface the limit, so we fetch the raw trustline ledger entry with getLedgerEntries.

Amounts are stored on the ledger as int64 stroops, and the largest representable amount — 922,337,203,685.4775807 — exceeds what a JavaScript Number can hold exactly. Trustline limits routinely sit at that maximum, so this example keeps the arithmetic in BigInt stroops; converting to Number first can silently misjudge whether a payment fits. See Amount precision for the full picture.

import { Asset, Keypair, xdr } from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";

const rpc = new Server("https://soroban-testnet.stellar.org");

// The account you're about to pay, and the asset and amount you want to send.
const receiver = "G...";
const USDC = new Asset(
"USDC",
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
);

// Amounts live on the ledger as int64 stroops, whose maximum (9223372036854775807,
// or 922,337,203,685.4775807 units) is far beyond what a JavaScript `Number` can
// hold exactly. Convert the amount you want to send into stroops as a BigInt and
// do the comparison there, so a large trustline can't be misjudged.
function toStroops(amount) {
const [whole, fraction = ""] = amount.split(".");
return BigInt(whole + fraction.padEnd(7, "0").slice(0, 7));
}

const sendingAmount = toStroops("1");

// A ledger key uniquely identifies an entry on the ledger. For a trustline, it
// is the combination of the account that holds it and the asset it is for.
const key = xdr.LedgerKey.trustline(
new xdr.LedgerKeyTrustLine({
accountId: Keypair.fromPublicKey(receiver).xdrAccountId(),
asset: USDC.toTrustLineXDRObject(),
}),
);

// Query the ledger for that entry. If the account has no trustline for the
// asset, `entries` comes back empty rather than throwing.
const { entries } = await rpc.getLedgerEntries(key);

if (entries.length === 0) {
console.error(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} not found for account ${receiver}.`,
);
throw new Error("Trustline not found");
}

// `val` is the decoded ledger entry data, so we can read the trustline fields
// directly from it.
const trustlineData = entries[0].val.trustLine();

// Trustline flags are a bitfield, so check the individual bit rather than
// comparing the whole value: 0x1 is AUTHORIZED_FLAG, 0x2 is
// AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG and 0x4 is
// TRUSTLINE_CLAWBACK_ENABLED_FLAG. An authorized, clawback-enabled trustline
// has flags of 5, so `flags() === 1` would wrongly reject it.
if (!(trustlineData.flags() & 1)) {
console.error(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} is not authorized for account ${receiver}.`,
);
throw new Error("Trustline not authorized");
}

// Read the limit and balance as BigInt stroops, with no conversion to Number.
const limit = trustlineData.limit().toBigInt();
const balance = trustlineData.balance().toBigInt();

// Finally, we check if the trustline has enough limit to receive the payment.
// We compare the trustline's limit minus its current balance with the amount we
// want to send. Attempting to send an amount that exceeds the available limit
// will result in a failed transaction, therefore, if the limit is insufficient,
// we log an error and throw an exception.
if (limit - balance < sendingAmount) {
console.error(
`Insufficient limit for asset ${USDC.code} issued by ${USDC.issuer} in account ${receiver}.`,
);
throw new Error("Insufficient limit for asset");
}

console.log(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} is valid for account ${receiver}.`,
);

// Proceed with sending the payment...

Checking a Trustline through the Stellar Asset Contract (SAC)

All Stellar assets, including the native asset (XLM), can be managed with smart contract transactions through Stellar Asset Contracts (SAC). SACs provide a smart contract interface for handling assets, allowing for more complex interactions and programmability. This means that it includes certain functions to help developers manage assets, such as verifying trustlines and sending payments, in a more flexible way than classic operations.

For this example, we'll be using SAC as a smart contract interface for the testnet USDC asset. A contract invocation transaction will be made to call the function authorized, which returns a boolean indicating whether the account's trustline is authorized.

This function can be accessed directly in a smart contract invocation as the example below demonstrates, or it can also be invoked by another contract, allowing for more complex interactions and programmability to be built in smart contracts.

Note that authorized only reports the trustline's authorization state. If the account has no trustline for the asset at all, the invocation does not return false — the simulation fails with Error(Contract, #13) (trustline entry is missing for account), so the two cases are handled in different branches below. If you also need to check the trustline's limit, use the getLedgerEntries approach shown above.

info

To use the RPC example below you should first generate the contract bindings so the client can be used accordingly. This can be achieved through the Stellar CLI.

E.g.: Generating the typescript bindings for the SAC of a given asset. The generated package takes its name from the output directory, so --output-dir=./sac is what makes import ... from "sac" below resolve:

stellar contract bindings typescript --network=testnet --contract-id=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA --output-dir=./sac

Given a receiver address, the following code snippet demonstrates how to simulate a transaction to check if a trustline exists for a specific asset:

import { Asset, Networks } from "@stellar/stellar-sdk";
import { Client } from "sac";
import { Server } from "@stellar/stellar-sdk/rpc";

// Initialize Stellar RPC server for testnet
const rpc = new Server("https://soroban-testnet.stellar.org");

// The account you're about to pay. Replace this with your real destination.
const receiver = "G...";

// First, check to make sure that the destination account exists.
try {
await rpc.getAccount(receiver);
} catch (error) {
console.error("Error checking destination account:", error);
throw error;
}

// Now we define which asset we want to check the trustline for.
// In this case, we are checking for USDC issued on testnet.
const USDC = new Asset(
"USDC",
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
);

// Now we initialize the Stellar Asset Contract (SAC) client.
// The client needs the RPC endpoint, network passphrase, contract ID for the asset,
// and your account's public key. Since we are only going to simulate the transaction,
// we do not need to provide a signing function.
const usdcClient = new Client({
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: Networks.TESTNET,
contractId: USDC.contractId(Networks.TESTNET),
publicKey: receiver,
});

// Now, using the client, we assemble a soroban transaction to invoke the
// `authorized` function of the USDC asset contract. The client will automatically
// bundle the operation and simulate the transaction before providing us
// with an assembled transaction object. This object contains the result of the simulation,
// which we can check to see if the trustline is authorized or not.
let assembledTx;
try {
assembledTx = await usdcClient.authorized({
id: receiver,
});

// The result parameter contains the return value of the contract function.
// If the trustline is authorized, it will return true; otherwise, it will return false.
const result = assembledTx.result;

// A `false` result means the trustline exists but the issuer has not
// authorized it, so any attempt to send USDC to this account will fail.
if (result === false) {
console.error(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} not authorized for account ${receiver}.`,
);
throw new Error("Trustline not authorized");
}

// If the trustline is authorized, we log a success message.
// This means that the account is allowed to receive payments in USDC.
console.log(
`Trustline for asset ${USDC.code} issued by ${USDC.issuer} is authorized for account ${receiver}.`,
);
} catch (error) {
// A missing trustline surfaces here rather than as a `false` result: the
// simulation fails with `Error(Contract, #13)`.
console.error("Error assembling and simulating the transaction:", error);
throw error;
}

// If the trustline is authorized, we can proceed with sending the payment...