Trust Lines and Issued Currencies | XRPL Development 101 | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
intermediate55 min

Trust Lines and Issued Currencies

Learning Objectives

Explain the trust line model and how it differs from ERC-20 tokens

Create trust lines programmatically between accounts

Issue tokens on XRPL and understand issuer responsibilities

Configure trust line settings including limits, rippling, and authorization

Implement freezing capabilities for compliance requirements

Here's a fundamental truth about tokenized assets: if you hold a token representing USD, you're trusting someone to honor it.

Ethereum's ERC-20 model hides this: you hold USDC in your wallet, and trust is implicit. XRPL makes trust explicit through trust lines—bilateral agreements between accounts about what IOUs they'll accept and from whom.

  • You can't receive tokens you haven't agreed to accept
  • You explicitly cap your exposure to any issuer
  • The "rippling" mechanism enables efficient multi-hop payments
  • Issuers have built-in compliance tools (freeze, authorize)

Let's understand how this works.


Think of a trust line as a credit line at a bank:

  • Bank says: "We'll honor up to $10,000 of IOUs from you"

  • You say: "I'll honor up to $500 of IOUs from you"

  • These are independent decisions

  • Account A says: "I'll hold up to 1000 USD from Issuer B"

  • Issuer B says: "I'll issue up to 1000000 USD total"

  • Balance flows between them as transactions occur

Key Concept

Key insight

A trust line doesn't give you tokens. It says you're *willing* to receive tokens up to a limit. The issuer then credits your balance.

Each trust line is stored as a RippleState ledger object:

// RippleState object (simplified)
{
    "LedgerEntryType": "RippleState",
    "Balance": {
        "currency": "USD",
        "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji",  // Neutral (neither party)
        "value": "100"  // Positive = holder has 100 USD from issuer
    },
    "HighLimit": {
        "currency": "USD",
        "issuer": "rHolder...",    // Higher address (alphabetically)
        "value": "1000"            // Holder's limit
    },
    "LowLimit": {
        "currency": "USD",
        "issuer": "rIssuer...",    // Lower address (alphabetically)
        "value": "0"               // Issuer's limit (usually 0)
    },
    "Flags": 131072               // Various settings
}

Why High/Low? XRPL stores each trust line once, from the perspective of the "low" account (alphabetically lower address). The balance sign indicates direction.

Balance Positive: "High" account holds IOUs issued by "Low" account
Balance Negative: "Low" account holds IOUs issued by "High" account
  • rHolder (alphabetically higher) trusts rIssuer (lower)
  • Balance: +100 means rHolder has 100 tokens
  • Balance: -100 means rIssuer has 100 tokens (rare for typical issuance)

// src/tokens/create-trustline.js
const xrpl = require('xrpl');

async function createTrustLine(wallet, issuer, currency, limit) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

try {
const trustSet = {
TransactionType: 'TrustSet',
Account: wallet.address,
LimitAmount: {
currency: currency, // 3-char code or 40-char hex
issuer: issuer, // Issuer's address
value: limit.toString() // Max you'll hold
}
};

console.log(Creating trust line for ${currency} from ${issuer});
console.log(Limit: ${limit});

const prepared = await client.autofill(trustSet);
const signed = wallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);

const txResult = result.result.meta.TransactionResult;

if (txResult === 'tesSUCCESS') {
console.log('✓ Trust line created successfully');
return {
success: true,
hash: signed.hash,
ledger: result.result.ledger_index
};
} else {
console.log('✗ Trust line creation failed:', txResult);
return {
success: false,
resultCode: txResult
};
}

} finally {
await client.disconnect();
}
}

// Example: Trust USD from a gateway
async function main() {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

// Create holder account
const holder = xrpl.Wallet.generate();
await client.fundWallet(holder);
console.log('Holder:', holder.address);

// Create issuer account
const issuer = xrpl.Wallet.generate();
await client.fundWallet(issuer);
console.log('Issuer:', issuer.address);

await client.disconnect();

// Holder creates trust line to issuer
await createTrustLine(holder, issuer.address, 'USD', '10000');
}

module.exports = { createTrustLine };

if (require.main === module) {
main().catch(console.error);
}
```

# src/tokens/create_trustline.py
import asyncio
from xrpl.asyncio.clients import AsyncWebsocketClient
from xrpl.wallet import Wallet
from xrpl.models.transactions import TrustSet
from xrpl.models.amounts import IssuedCurrencyAmount
from xrpl.asyncio.transaction import submit_and_wait, autofill

async def create_trust_line(
wallet: Wallet,
issuer: str,
currency: str,
limit: str
) -> dict:
"""Create a trust line to hold issued currency.

Args:
wallet: The wallet creating the trust line
issuer: Address of the token issuer
currency: Currency code (3 chars or 40 hex chars)
limit: Maximum amount willing to hold

Returns:
Transaction result
"""
url = "wss://s.altnet.rippletest.net:51233"

async with AsyncWebsocketClient(url) as client:
trust_set = TrustSet(
account=wallet.classic_address,
limit_amount=IssuedCurrencyAmount(
currency=currency,
issuer=issuer,
value=limit
)
)

print(f"Creating trust line for {currency} from {issuer}")
print(f"Limit: {limit}")

prepared = await autofill(trust_set, client)
signed = wallet.sign(prepared)
result = await submit_and_wait(signed, client)

tx_result = result.result.get("meta", {}).get("TransactionResult")

if tx_result == "tesSUCCESS":
print("✓ Trust line created successfully")
return {
"success": True,
"hash": signed.hash,
"ledger": result.result.get("ledger_index")
}
else:
print(f"✗ Trust line creation failed: {tx_result}")
return {
"success": False,
"result_code": tx_result
}
```

XRPL supports two currency code formats:

// Standard codes: 3 ASCII characters (case-sensitive)
const standardCodes = ['USD', 'EUR', 'BTC', 'ETH'];

// Hex codes: 40 hexadecimal characters for custom tokens
// Allows longer names and special characters
function currencyToHex(name) {
    // Pad to 20 bytes, convert to hex
    const padded = name.padEnd(20, '\0');
    return Buffer.from(padded).toString('hex').toUpperCase();
}

// Examples:
// "MYTOKEN" → "4D59544F4B454E0000000000000000000000000000"
// Allows tokens like "USDC", "RLUSD", or any custom name

// Note: "XRP" is special - it refers to native XRP, not an issued currency
// You cannot create a token called "XRP"

  • Creates the currency by sending payments
  • Can freeze balances (if not waived)
  • Can require authorization for trust lines
  • Can charge transfer fees
  • Maintains obligations to redeem tokens
Issuance Flow:

1. Holder creates trust line to Issuer (TrustSet)

1. Issuer sends payment to Holder (Payment)

1. Holder's trust line balance: +100 USD

1. Holder can now:
// src/tokens/issue-tokens.js
const xrpl = require('xrpl');

async function issueTokens(issuerWallet, recipientAddress, currency, amount) {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

try {
// First verify recipient has a trust line
const lines = await client.request({
command: 'account_lines',
account: recipientAddress,
peer: issuerWallet.address
});

const trustLine = lines.result.lines.find(
line => line.currency === currency
);

if (!trustLine) {
throw new Error(Recipient has no trust line for ${currency});
}

if (Number(trustLine.limit) < Number(amount)) {
throw new Error(Recipient limit (${trustLine.limit}) less than amount (${amount}));
}

// Issue by sending payment
const payment = {
TransactionType: 'Payment',
Account: issuerWallet.address,
Destination: recipientAddress,
Amount: {
currency: currency,
issuer: issuerWallet.address,
value: amount.toString()
}
};

console.log(Issuing ${amount} ${currency} to ${recipientAddress});

const prepared = await client.autofill(payment);
const signed = issuerWallet.sign(prepared);
const result = await client.submitAndWait(signed.tx_blob);

const txResult = result.result.meta.TransactionResult;

if (txResult === 'tesSUCCESS') {
console.log('✓ Tokens issued successfully');
return { success: true, hash: signed.hash };
} else {
console.log('✗ Issuance failed:', txResult);
return { success: false, resultCode: txResult };
}

} finally {
await client.disconnect();
}
}

// Complete example: Set up issuer and issue tokens
async function demonstrateIssuance() {
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client.connect();

// Create issuer
const issuer = xrpl.Wallet.generate();
await client.fundWallet(issuer);
console.log('Issuer:', issuer.address);

// Create holder
const holder = xrpl.Wallet.generate();
await client.fundWallet(holder);
console.log('Holder:', holder.address);

await client.disconnect();

// Step 1: Holder creates trust line
await createTrustLine(holder, issuer.address, 'USD', '10000');

// Step 2: Issuer sends tokens
await issueTokens(issuer, holder.address, 'USD', '500');

// Step 3: Verify balance
const client2 = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
await client2.connect();

const lines = await client2.request({
command: 'account_lines',
account: holder.address
});

console.log('\nHolder balances:');
for (const line of lines.result.lines) {
console.log( ${line.currency}: ${line.balance});
}

await client2.disconnect();
}

module.exports = { issueTokens };
```


Rippling allows issued currencies to flow through intermediary accounts:

Without Rippling:
Alice has 100 USD from Gateway
Bob wants USD from Gateway
Alice must send directly to Gateway, Gateway sends to Bob

With Rippling:
Alice has 100 USD from Gateway
Bob trusts Gateway for USD
Alice sends 50 USD to Bob
Result: Alice has 50 USD, Bob has 50 USD
The Gateway's total obligation stays the same!

This enables efficient payments without the issuer being involved in every transaction.

By default, rippling is enabled. The NoRipple flag disables it:

// src/tokens/rippling-settings.js
const xrpl = require('xrpl');

async function setNoRipple(wallet, issuer, currency, enableNoRipple = true) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const flags = enableNoRipple 
            ? { tfSetNoRipple: true }
            : { tfClearNoRipple: true };

const trustSet = {
            TransactionType: 'TrustSet',
            Account: wallet.address,
            LimitAmount: {
                currency: currency,
                issuer: issuer,
                value: '10000'  // Must include limit
            },
            Flags: enableNoRipple ? 131072 : 262144  // tfSetNoRipple or tfClearNoRipple
        };

const prepared = await client.autofill(trustSet);
        const signed = wallet.sign(prepared);
        const result = await client.submitAndWait(signed.tx_blob);

console.log(`NoRipple ${enableNoRipple ? 'enabled' : 'disabled'}:`, 
                    result.result.meta.TransactionResult);

return result;

} finally {
        await client.disconnect();
    }
}

// When to use NoRipple:
// ✓ You're a regular holder, not a market maker
// ✓ You don't want your balance used as a path for others
// ✓ You want to prevent unexpected balance changes

// When NOT to use NoRipple:
// ✗ You're a market maker providing liquidity
// ✗ You're an issuer (should use DefaultRipple instead)
// ✗ You want to enable efficient payment routing

Issuers should enable DefaultRipple on their account:

async function enableDefaultRipple(issuerWallet) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const accountSet = {
            TransactionType: 'AccountSet',
            Account: issuerWallet.address,
            SetFlag: xrpl.AccountSetAsfFlags.asfDefaultRipple  // 8
        };

const prepared = await client.autofill(accountSet);
        const signed = issuerWallet.sign(prepared);
        const result = await client.submitAndWait(signed.tx_blob);

console.log('DefaultRipple enabled:', 
                    result.result.meta.TransactionResult);

return result;

} finally {
        await client.disconnect();
    }
}

Require approval before accounts can hold your tokens:

async function enableRequireAuth(issuerWallet) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const accountSet = {
            TransactionType: 'AccountSet',
            Account: issuerWallet.address,
            SetFlag: xrpl.AccountSetAsfFlags.asfRequireAuth  // 2
        };

const prepared = await client.autofill(accountSet);
        const signed = issuerWallet.sign(prepared);
        await client.submitAndWait(signed.tx_blob);

console.log('RequireAuth enabled - must authorize each trust line');

} finally {
        await client.disconnect();
    }
}

async function authorizeTrustLine(issuerWallet, holderAddress, currency) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        // Issuer creates a trust line to the holder with zero limit
        // This authorizes the holder's trust line
        const trustSet = {
            TransactionType: 'TrustSet',
            Account: issuerWallet.address,
            LimitAmount: {
                currency: currency,
                issuer: holderAddress,  // Note: holder address here
                value: '0'              // Zero limit = authorization only
            },
            Flags: 65536  // tfSetfAuth
        };

const prepared = await client.autofill(trustSet);
        const signed = issuerWallet.sign(prepared);
        const result = await client.submitAndWait(signed.tx_blob);

console.log(`Authorized ${holderAddress} for ${currency}`);
        return result;

} finally {
        await client.disconnect();
    }
}

Issuers can freeze individual trust lines or all tokens globally:

// Freeze individual trust line
async function freezeTrustLine(issuerWallet, holderAddress, currency) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const trustSet = {
            TransactionType: 'TrustSet',
            Account: issuerWallet.address,
            LimitAmount: {
                currency: currency,
                issuer: holderAddress,
                value: '0'
            },
            Flags: 1048576  // tfSetFreeze
        };

const prepared = await client.autofill(trustSet);
        const signed = issuerWallet.sign(prepared);
        await client.submitAndWait(signed.tx_blob);

console.log(`Frozen trust line for ${holderAddress}`);

} finally {
        await client.disconnect();
    }
}

// Global freeze - freezes ALL tokens from this issuer
async function globalFreeze(issuerWallet, enable = true) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const accountSet = {
            TransactionType: 'AccountSet',
            Account: issuerWallet.address,
            [enable ? 'SetFlag' : 'ClearFlag']: 
                xrpl.AccountSetAsfFlags.asfGlobalFreeze  // 7
        };

const prepared = await client.autofill(accountSet);
        const signed = issuerWallet.sign(prepared);
        await client.submitAndWait(signed.tx_blob);

console.log(`Global freeze ${enable ? 'enabled' : 'disabled'}`);

} finally {
        await client.disconnect();
    }
}

// NoFreeze - permanently give up freeze ability
async function permanentlyDisableFreeze(issuerWallet) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        // WARNING: This is IRREVERSIBLE
        const accountSet = {
            TransactionType: 'AccountSet',
            Account: issuerWallet.address,
            SetFlag: xrpl.AccountSetAsfFlags.asfNoFreeze  // 6
        };

const prepared = await client.autofill(accountSet);
        const signed = issuerWallet.sign(prepared);
        await client.submitAndWait(signed.tx_blob);

console.log('NoFreeze enabled - freeze capability permanently disabled');

} finally {
        await client.disconnect();
    }
}

Issuers can charge fees when their tokens are transferred:

async function setTransferFee(issuerWallet, feePercent) {
    // Fee is in units of 1/10 billionth
    // 1% = 1000000000 (1 billion)
    // Max fee: 100% (1e9 * 100)
    // Min fee: 0

if (feePercent < 0 || feePercent > 100) {
        throw new Error('Fee must be between 0 and 100 percent');
    }

// Convert percentage to transfer rate
    // transferRate = 1000000000 * (1 + fee/100)
    // For 1% fee: 1010000000
    const transferRate = Math.floor(1000000000 * (1 + feePercent / 100));

const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const accountSet = {
            TransactionType: 'AccountSet',
            Account: issuerWallet.address,
            TransferRate: transferRate
        };

const prepared = await client.autofill(accountSet);
        const signed = issuerWallet.sign(prepared);
        await client.submitAndWait(signed.tx_blob);

console.log(`Transfer fee set to ${feePercent}%`);

} finally {
        await client.disconnect();
    }
}

Each trust line costs 2 XRP in owner reserve:

async function checkReserveImpact(address) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        const info = await client.request({
            command: 'account_info',
            account: address
        });

const data = info.result.account_data;
        const balance = Number(data.Balance) / 1_000_000;
        const ownerCount = data.OwnerCount;
        const baseReserve = 10;
        const ownerReserve = ownerCount * 2;

console.log(`Account: ${address}`);
        console.log(`Balance: ${balance} XRP`);
        console.log(`Owned objects: ${ownerCount}`);
        console.log(`Reserved: ${baseReserve + ownerReserve} XRP`);
        console.log(`Available: ${balance - baseReserve - ownerReserve} XRP`);
        console.log(`\nAdding a trust line would reserve additional 2 XRP`);

return {
            balance,
            reserved: baseReserve + ownerReserve,
            available: balance - baseReserve - ownerReserve
        };

} finally {
        await client.disconnect();
    }
}

To remove a trust line and reclaim reserve:

async function removeTrustLine(wallet, issuer, currency) {
    const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
    await client.connect();

try {
        // First check balance is zero
        const lines = await client.request({
            command: 'account_lines',
            account: wallet.address,
            peer: issuer
        });

const line = lines.result.lines.find(l => l.currency === currency);

if (!line) {
            console.log('Trust line does not exist');
            return;
        }

if (Number(line.balance) !== 0) {
            throw new Error(`Cannot remove: balance is ${line.balance}`);
        }

// Set limit to zero to remove
        const trustSet = {
            TransactionType: 'TrustSet',
            Account: wallet.address,
            LimitAmount: {
                currency: currency,
                issuer: issuer,
                value: '0'
            }
        };

const prepared = await client.autofill(trustSet);
        const signed = wallet.sign(prepared);
        const result = await client.submitAndWait(signed.tx_blob);

console.log('Trust line removed:', 
                    result.result.meta.TransactionResult);

} finally {
        await client.disconnect();
    }
}

XRPL's trust line model is more transparent than ERC-20 tokens but requires users to actively manage trust relationships. This explicit trust model is powerful for compliance but adds friction for casual users. Understanding rippling is essential—it's both XRPL's superpower for efficient payments and a source of confusion for newcomers.


Assignment: Build a complete token issuance system with compliance features.

Requirements:

  • Create issuer account with proper settings

  • Enable DefaultRipple

  • Optionally enable RequireAuth

  • Set transfer fee (choose appropriate rate)

  • Script to create trust lines with configurable limits

  • Handle authorization if RequireAuth is enabled

  • Display trust line status and settings

  • Issue tokens to accounts with trust lines

  • Verify successful issuance

  • Display issuer's total obligations (negative balance sum)

  • Implement individual freeze/unfreeze

  • Implement global freeze (with confirmation)

  • Display frozen status

  • Issuer setup correct with appropriate settings (25%)

  • Trust line creation handles all cases (25%)

  • Token issuance works reliably (25%)

  • Compliance features functional (25%)

Time investment: 3 hours
Value: This forms the foundation for any token-based application on XRPL


Knowledge Check

Question 1 of 3

Alice has 100 USD from Gateway. Bob trusts Gateway for USD. With rippling enabled, what happens if Alice sends 50 USD to Bob?

For Next Lesson:
Now that you understand trust lines, Lesson 7 covers sending payments in issued currencies—including cross-currency payments, pathfinding, and handling the complexities of multi-hop transfers.


End of Lesson 6

Total words: ~5,200
Estimated completion time: 55 minutes reading + 3 hours for deliverable

Key Takeaways

1

Trust lines are bilateral agreements

: You must explicitly agree to hold tokens from an issuer. This prevents unwanted tokens but requires setup.

2

Issuers have obligations

: When you issue tokens, you're creating liabilities. The balance is negative from your perspective.

3

Rippling enables efficiency but needs management

: Enable NoRipple on personal accounts; enable DefaultRipple on issuer accounts.

4

Compliance features are built-in

: Freeze, authorize, and transfer fees are native—no smart contract needed.

5

Reserve costs add up

: Each trust line costs 2 XRP. Plan for this when designing applications with many tokens. ---