diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..1ef6ad5e231 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "mcp__context7__query-docs" + ] + } +} diff --git a/_code-samples/cross-currency-payment/js/.gitignore b/_code-samples/cross-currency-payment/js/.gitignore new file mode 100644 index 00000000000..48f3676e90f --- /dev/null +++ b/_code-samples/cross-currency-payment/js/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +crossCurrencySetup.json diff --git a/_code-samples/cross-currency-payment/js/README.md b/_code-samples/cross-currency-payment/js/README.md new file mode 100644 index 00000000000..41b9b5ee8f7 --- /dev/null +++ b/_code-samples/cross-currency-payment/js/README.md @@ -0,0 +1,48 @@ +# Cross-Currency Payment Examples (JavaScript) + +Sends a cross-currency payment where the source spends XRP and the destination is credited in USD, converted through the DEX order book. + +## Setup + +```sh +npm i +``` + +## Send a Cross-Currency Payment + +```sh +node sendCrossCurrency.js +``` + +Delivers 100 USD to the receiver, spending up to 30 XRP from the source, and prints the `delivered_amount` from the transaction metadata. If `crossCurrencySetup.json` is missing, the script runs `crossCurrencySetup.js` first to create the issuer, market maker, sender, and receiver accounts and to post the XRP/USD liquidity. + +```sh +Sender address: rSENDER... +Receiver address: rRECEIVER... + +=== Preparing cross-currency Payment === + +{ + "TransactionType": "Payment", + "Account": "rSENDER...", + "Destination": "rRECEIVER...", + "Amount": { + "currency": "USD", + "issuer": "rISSUER...", + "value": "100" + }, + "SendMax": "30000000", + "DeliverMin": { + "currency": "USD", + "issuer": "rISSUER...", + "value": "95" + }, + "Flags": 131072 +} + +=== Submitting cross-currency Payment === + +=== Payment delivered === + +delivered_amount: { currency: 'USD', issuer: 'rISSUER...', value: '100' } +``` diff --git a/_code-samples/cross-currency-payment/js/crossCurrencySetup.js b/_code-samples/cross-currency-payment/js/crossCurrencySetup.js new file mode 100644 index 00000000000..6edfdadb2f8 --- /dev/null +++ b/_code-samples/cross-currency-payment/js/crossCurrencySetup.js @@ -0,0 +1,75 @@ +// Setup script for cross-currency payment tutorial +import xrpl from 'xrpl' +import fs from 'fs' + +const WSS_URL = 'wss://s.altnet.rippletest.net:51233' +const CURRENCY = 'USD' + +const client = new xrpl.Client(WSS_URL) +await client.connect() + +// Fund the four wallets the tutorial needs +process.stdout.write('Setting up tutorial: 1/5\r') +const [ + { wallet: issuer }, + { wallet: marketMaker }, + { wallet: sender }, + { wallet: receiver } +] = await Promise.all([ + client.fundWallet(), + client.fundWallet(), + client.fundWallet(), + client.fundWallet() +]) + +// Enable rippling so the issuer's USD can move between holders +process.stdout.write('Setting up tutorial: 2/5\r') +await client.submitAndWait({ + TransactionType: 'AccountSet', + Account: issuer.address, + SetFlag: 8 // asfDefaultRipple +}, { wallet: issuer, autofill: true }) + +// Market maker and receiver trust the issuer's USD +process.stdout.write('Setting up tutorial: 3/5\r') +await Promise.all([ + client.submitAndWait({ + TransactionType: 'TrustSet', + Account: marketMaker.address, + LimitAmount: { currency: CURRENCY, issuer: issuer.address, value: '1000000' } + }, { wallet: marketMaker, autofill: true }), + client.submitAndWait({ + TransactionType: 'TrustSet', + Account: receiver.address, + LimitAmount: { currency: CURRENCY, issuer: issuer.address, value: '1000000' } + }, { wallet: receiver, autofill: true }) +]) + +// Issue USD to the market maker so it has inventory to sell +process.stdout.write('Setting up tutorial: 4/5\r') +await client.submitAndWait({ + TransactionType: 'Payment', + Account: issuer.address, + Destination: marketMaker.address, + Amount: { currency: CURRENCY, issuer: issuer.address, value: '100000' } +}, { wallet: issuer, autofill: true }) + +// Post an offer providing XRP -> USD liquidity (4 USD per XRP) +process.stdout.write('Setting up tutorial: 5/5\r') +await client.submitAndWait({ + TransactionType: 'OfferCreate', + Account: marketMaker.address, + TakerGets: { currency: CURRENCY, issuer: issuer.address, value: '10000' }, + TakerPays: xrpl.xrpToDrops('2500') +}, { wallet: marketMaker, autofill: true }) + +const setupData = { + description: 'Auto-generated by crossCurrencySetup.js. Stores XRPL account info for the cross-currency payment tutorial.', + issuerAddress: issuer.address, + currency: CURRENCY, + sender: { address: sender.address, seed: sender.seed }, + receiver: { address: receiver.address, seed: receiver.seed } +} +fs.writeFileSync('crossCurrencySetup.json', JSON.stringify(setupData, null, 2)) + +await client.disconnect() diff --git a/_code-samples/cross-currency-payment/js/package.json b/_code-samples/cross-currency-payment/js/package.json new file mode 100644 index 00000000000..65732983fe4 --- /dev/null +++ b/_code-samples/cross-currency-payment/js/package.json @@ -0,0 +1,8 @@ +{ + "name": "cross-currency-payment-examples", + "description": "Example code for sending a cross-currency payment on the XRP Ledger.", + "dependencies": { + "xrpl": "^4.6.0" + }, + "type": "module" +} diff --git a/_code-samples/cross-currency-payment/js/sendCrossCurrency.js b/_code-samples/cross-currency-payment/js/sendCrossCurrency.js new file mode 100644 index 00000000000..d98e6fe458a --- /dev/null +++ b/_code-samples/cross-currency-payment/js/sendCrossCurrency.js @@ -0,0 +1,63 @@ +// IMPORTANT: This script sends a cross-currency payment. The sender spends XRP +// and the receiver is credited in USD, converted through the DEX order book. +// It requires the accounts and liquidity created by crossCurrencySetup.js; if +// the setup data is missing, this script runs the setup script first. +import xrpl from 'xrpl' +import fs from 'fs' +import { execSync } from 'child_process' + +// Connect to the network ---------------------- +const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233') +await client.connect() + +// Ensure the tutorial's accounts and liquidity exist ---------------------- +if (!fs.existsSync('crossCurrencySetup.json')) { + console.log(`\n=== Setup data doesn't exist. Running setup script... ===\n`) + execSync('node crossCurrencySetup.js', { stdio: 'inherit' }) +} + +const setupData = JSON.parse(fs.readFileSync('crossCurrencySetup.json', 'utf8')) +const sender = xrpl.Wallet.fromSeed(setupData.sender.seed) +const receiverAddress = setupData.receiver.address +const issuerAddress = setupData.issuerAddress +const currency = setupData.currency + +console.log(`\nSender address: ${sender.address}`) +console.log(`Receiver address: ${receiverAddress}`) + +// Prepare cross-currency Payment ---------------------- +// Deliver 100 USD to the receiver while spending no more than 30 XRP from the +// source. The ledger routes XRP -> USD through the DEX automatically, so there +// is no router to integrate. SendMax caps what the source spends. DeliverMin, +// together with the tfPartialPayment flag, floors what the destination receives. +console.log(`\n=== Preparing cross-currency Payment ===\n`) +const paymentTx = { + TransactionType: 'Payment', + Account: sender.address, + Destination: receiverAddress, + Amount: { currency, issuer: issuerAddress, value: '100' }, + SendMax: xrpl.xrpToDrops('30'), + DeliverMin: { currency, issuer: issuerAddress, value: '95' }, + Flags: 131072 // tfPartialPayment +} + +xrpl.validate(paymentTx) +console.log(JSON.stringify(paymentTx, null, 2)) + +// Submit, sign, and wait for validation ---------------------- +console.log(`\n=== Submitting cross-currency Payment ===\n`) +const response = await client.submitAndWait(paymentTx, { wallet: sender, autofill: true }) + +const resultCode = response.result.meta.TransactionResult +if (resultCode !== 'tesSUCCESS') { + console.error('Error: payment failed with', resultCode) + await client.disconnect() + process.exit(1) +} + +// Credit the amount that ACTUALLY arrived, from metadata, never the Amount field +console.log(`\n=== Payment delivered ===\n`) +console.log('delivered_amount:', response.result.meta.delivered_amount) +// End cross-currency payment + +await client.disconnect() diff --git a/_code-samples/cross-currency-payment/py/.gitignore b/_code-samples/cross-currency-payment/py/.gitignore new file mode 100644 index 00000000000..5994c37d4bd --- /dev/null +++ b/_code-samples/cross-currency-payment/py/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +cross_currency_setup.json diff --git a/_code-samples/cross-currency-payment/py/README.md b/_code-samples/cross-currency-payment/py/README.md new file mode 100644 index 00000000000..2c5c0aca226 --- /dev/null +++ b/_code-samples/cross-currency-payment/py/README.md @@ -0,0 +1,50 @@ +# Cross-Currency Payment Examples (Python) + +Sends a cross-currency payment where the source spends XRP and the destination is credited in USD, converted through the DEX order book. + +## Setup + +```sh +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## Send a Cross-Currency Payment + +```sh +python3 send_cross_currency.py +``` + +Delivers 100 USD to the receiver, spending up to 30 XRP from the source, and prints the `delivered_amount` from the transaction metadata. If `cross_currency_setup.json` is missing, the script runs `cross_currency_setup.py` first to create the issuer, market maker, sender, and receiver accounts and to post the XRP/USD liquidity. + +```sh +Sender address: rSENDER... +Receiver address: rRECEIVER... + +=== Preparing cross-currency Payment === + +{ + "Account": "rSENDER...", + "TransactionType": "Payment", + "Amount": { + "currency": "USD", + "issuer": "rISSUER...", + "value": "100" + }, + "Destination": "rRECEIVER...", + "SendMax": "30000000", + "DeliverMin": { + "currency": "USD", + "issuer": "rISSUER...", + "value": "95" + }, + "Flags": 131072 +} + +=== Submitting cross-currency Payment === + +=== Payment delivered === + +delivered_amount: {'currency': 'USD', 'issuer': 'rISSUER...', 'value': '100'} +``` diff --git a/_code-samples/cross-currency-payment/py/cross_currency_setup.py b/_code-samples/cross-currency-payment/py/cross_currency_setup.py new file mode 100644 index 00000000000..814b04c19bc --- /dev/null +++ b/_code-samples/cross-currency-payment/py/cross_currency_setup.py @@ -0,0 +1,119 @@ +# Setup script for cross-currency payment tutorial +import asyncio +import json + +from xrpl.asyncio.clients import AsyncWebsocketClient +from xrpl.asyncio.transaction import submit_and_wait +from xrpl.asyncio.wallet import generate_faucet_wallet +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.transactions import ( + AccountSet, + AccountSetAsfFlag, + OfferCreate, + Payment, + TrustSet, +) +from xrpl.utils import xrp_to_drops + +WSS_URL = "wss://s.altnet.rippletest.net:51233" +CURRENCY = "USD" + + +async def main(): + async with AsyncWebsocketClient(WSS_URL) as client: + # Fund the four wallets the tutorial needs + print("Setting up tutorial: 1/5", end="\r") + issuer, market_maker, sender, receiver = await asyncio.gather( + generate_faucet_wallet(client), + generate_faucet_wallet(client), + generate_faucet_wallet(client), + generate_faucet_wallet(client), + ) + + # Enable rippling so the issuer's USD can move between holders + print("Setting up tutorial: 2/5", end="\r") + await submit_and_wait( + AccountSet( + account=issuer.address, + set_flag=AccountSetAsfFlag.ASF_DEFAULT_RIPPLE, + ), + client, + issuer, + ) + + # Market maker and receiver trust the issuer's USD + print("Setting up tutorial: 3/5", end="\r") + await asyncio.gather( + submit_and_wait( + TrustSet( + account=market_maker.address, + limit_amount=IssuedCurrencyAmount( + currency=CURRENCY, + issuer=issuer.address, + value="1000000", + ), + ), + client, + market_maker, + ), + submit_and_wait( + TrustSet( + account=receiver.address, + limit_amount=IssuedCurrencyAmount( + currency=CURRENCY, + issuer=issuer.address, + value="1000000", + ), + ), + client, + receiver, + ), + ) + + # Issue USD to the market maker so it has inventory to sell + print("Setting up tutorial: 4/5", end="\r") + await submit_and_wait( + Payment( + account=issuer.address, + destination=market_maker.address, + amount=IssuedCurrencyAmount( + currency=CURRENCY, + issuer=issuer.address, + value="100000", + ), + ), + client, + issuer, + ) + + # Post an offer providing XRP -> USD liquidity (4 USD per XRP) + print("Setting up tutorial: 5/5", end="\r") + await submit_and_wait( + OfferCreate( + account=market_maker.address, + taker_gets=IssuedCurrencyAmount( + currency=CURRENCY, + issuer=issuer.address, + value="10000", + ), + taker_pays=xrp_to_drops(2500), + ), + client, + market_maker, + ) + + setup_data = { + "description": ( + "Auto-generated by cross_currency_setup.py. Stores XRPL account " + "info for the cross-currency payment tutorial." + ), + "issuer_address": issuer.address, + "currency": CURRENCY, + "sender": {"address": sender.address, "seed": sender.seed}, + "receiver": {"address": receiver.address, "seed": receiver.seed}, + } + with open("cross_currency_setup.json", "w") as f: + json.dump(setup_data, f, indent=2) + + +asyncio.run(main()) diff --git a/_code-samples/cross-currency-payment/py/requirements.txt b/_code-samples/cross-currency-payment/py/requirements.txt new file mode 100644 index 00000000000..6550f9202ad --- /dev/null +++ b/_code-samples/cross-currency-payment/py/requirements.txt @@ -0,0 +1 @@ +xrpl-py>=4.5.0 diff --git a/_code-samples/cross-currency-payment/py/send_cross_currency.py b/_code-samples/cross-currency-payment/py/send_cross_currency.py new file mode 100644 index 00000000000..8212c7b6d3a --- /dev/null +++ b/_code-samples/cross-currency-payment/py/send_cross_currency.py @@ -0,0 +1,73 @@ +# IMPORTANT: This script sends a cross-currency payment. The sender spends XRP +# and the receiver is credited in USD, converted through the DEX order book. +# It requires the accounts and liquidity created by cross_currency_setup.py; if +# the setup data is missing, this script runs the setup script first. +import json +import os +import subprocess +import sys + +from xrpl.clients import JsonRpcClient +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.transactions import Payment, PaymentFlag +from xrpl.transaction import submit_and_wait +from xrpl.utils import xrp_to_drops +from xrpl.wallet import Wallet + +# Set up client ---------------------- +client = JsonRpcClient("https://s.altnet.rippletest.net:51234") + +# Ensure the tutorial's accounts and liquidity exist ---------------------- +if not os.path.exists("cross_currency_setup.json"): + print("\n=== Setup data doesn't exist. Running setup script... ===\n") + subprocess.run([sys.executable, "cross_currency_setup.py"], check=True) + +with open("cross_currency_setup.json") as f: + setup_data = json.load(f) + +sender = Wallet.from_seed(setup_data["sender"]["seed"]) +receiver_address = setup_data["receiver"]["address"] +issuer_address = setup_data["issuer_address"] +currency = setup_data["currency"] + +print(f"\nSender address: {sender.address}") +print(f"Receiver address: {receiver_address}") + +# Prepare cross-currency Payment ---------------------- +# Deliver 100 USD to the receiver while spending no more than 30 XRP from the +# source. The ledger routes XRP -> USD through the DEX automatically, so there +# is no router to integrate. SendMax caps what the source spends. DeliverMin, +# together with the tfPartialPayment flag, floors what the destination receives. +print("\n=== Preparing cross-currency Payment ===\n") +payment_tx = Payment( + account=sender.address, + destination=receiver_address, + amount=IssuedCurrencyAmount( + currency=currency, + issuer=issuer_address, + value="100", + ), + send_max=xrp_to_drops(30), + deliver_min=IssuedCurrencyAmount( + currency=currency, + issuer=issuer_address, + value="95", + ), + flags=[PaymentFlag.TF_PARTIAL_PAYMENT], +) + +print(json.dumps(payment_tx.to_xrpl(), indent=2)) + +# Submit, sign, and wait for validation ---------------------- +print("\n=== Submitting cross-currency Payment ===\n") +response = submit_and_wait(payment_tx, client, sender) + +result_code = response.result["meta"]["TransactionResult"] +if result_code != "tesSUCCESS": + print(f"Error: payment failed with {result_code}") + sys.exit(1) + +# Credit the amount that ACTUALLY arrived, from metadata, never the Amount field +print("\n=== Payment delivered ===\n") +print("delivered_amount:", response.result["meta"]["delivered_amount"]) +# End cross-currency payment diff --git a/docs/_snippets/testnet-seed-warning.md b/docs/_snippets/testnet-seed-warning.md new file mode 100644 index 00000000000..59d0ebbc55e --- /dev/null +++ b/docs/_snippets/testnet-seed-warning.md @@ -0,0 +1,3 @@ +{% admonition type="warning" name="Caution" %} +This example handles seeds directly to keep the Testnet example simple. In production, never log or hardcode a seed. Store it in a secret manager and use [secure signing](../concepts/transactions/secure-signing.md). +{% /admonition %} diff --git a/docs/use-cases/payments/migrate-a-payments-or-fx-stack-to-the-xrp-ledger.md b/docs/use-cases/payments/migrate-a-payments-or-fx-stack-to-the-xrp-ledger.md new file mode 100644 index 00000000000..4dbd354e247 --- /dev/null +++ b/docs/use-cases/payments/migrate-a-payments-or-fx-stack-to-the-xrp-ledger.md @@ -0,0 +1,199 @@ +--- +seo: + description: Learn how to migrate your payments or FX stack to the XRP Ledger. +labels: + - Payments + - Cross-Currency +--- + +# Migrate a Payments or FX Stack to the XRP Ledger + +The XRP Ledger simplifies your [payments or FX stack](../payments/) migration by reducing the number of components you have to build, secure, and maintain yourself. There's no smart-contract layer to write; those capabilities are built into the protocol. + +If you're already running a payments or FX stack and want to move it to the XRP Ledger, this guide is for you. It assumes familiarity with your current payment flows and comfort with core XRP Ledger concepts, such as [accounts](../../concepts/accounts/index.md) and [transactions](../../concepts/transactions/index.md). + +Migrating mainly involves mapping your existing stack to native primitives, not rebuilding it. The seven steps below walk you through that mapping to a live pilot. + +## 1. Map Your Stack to XRP Ledger Primitives + +Many features that you'd implement as a smart contract on other chains are supported as native transaction types on the XRP Ledger, so the behavior is already built into the protocol. Before you port anything, examine which capabilities your stack uses and compare them to the XRP Ledger's built-in primitives: + +| What You Need | XRP Ledger Native Capability | +| --- | --- | +| Conditional or time-locked release of funds | [Escrow](../../concepts/payment-types/escrow.md) | +| Shared/multi-party control of an account | [Multi-signing](../../concepts/accounts/multi-signing.md) | +| Currency or asset issuance and management | [Issued Tokens](../../concepts/tokens/index.md) / [MPTs](../../concepts/tokens/fungible-tokens/multi-purpose-tokens.md) | +| On-chain currency conversion | [DEX](../../concepts/tokens/decentralized-exchange/index.md) order book, [AMM](../../concepts/tokens/decentralized-exchange/automated-market-makers.md), and [pathfinding](../../concepts/tokens/fungible-tokens/paths.md) | +| Compliance-gated trading | [Permissioned DEXes](../../concepts/tokens/decentralized-exchange/permissioned-dexes.md) | + +For a comprehensive list of built-in transaction types, visit the [Transaction Types](../../references/protocol/transactions/types/index.md) reference. Some primitives depend on recently enabled [amendments](../../concepts/networks-and-servers/amendments.md), so check the [amendment status](/resources/known-amendments.md) on your target network before you design around it. + +## 2. Choose Your Infrastructure Path + +Once you've mapped the capabilities to XRP Ledger primitives, you have to decide how to connect to the network. + +- Run your own ([`xrpld`](../../concepts/networks-and-servers/xrpld-server-modes.md) and [Clio](../../concepts/networks-and-servers/the-clio-server.md)): Most control over ledger history, query privacy, and throughput, without third-party dependencies. Requires high operational overhead for hardware and maintaining synced servers. See [Install and Run](../../infrastructure/index.md). +- Use a managed provider: Fastest way to ship with low upfront costs; someone else runs the nodes while you hold your keys and signing. Provides less control over [history retention](../../concepts/networks-and-servers/ledger-history.md), query privacy, and configuration, and you depend on their SLA. +- Use a custodial abstraction: Lowest security load and fastest integration by handing the custodian your keys and signing. Provides least control over funds and features, vendor lock-in, and potential compliance implications. + +The best path depends on your team's infrastructure experience and custody preferences. See the [payments overview](../payments/) for build-versus-partner guidance. + +## 3. Pick a Client Library + +After selecting your infrastructure, you need to decide which [library](../../references/client-libraries.md) you'll use to build, sign, and submit transactions and read the ledger state. + +If you're using a custodial abstraction, the custodian's API is typically your client library, so you can skip ahead to [Step 4](#4-set-up-your-accounts). + +The best place to start is by matching the library to your backend's primary language. XRPLF maintains `xrpl.js` (JavaScript/TypeScript), `xrpl-py` (Python), and `xrpl4j` (Java), which track new [amendments](../../concepts/networks-and-servers/amendments.md) the fastest. + +Other libraries, such as `xrpl-go` (Go) and `XRPL_PHP` (PHP), are community-maintained. Check the repositories for recent releases and confirm they support the amendments and features your integration needs before committing to one. + +If you don't find a library that matches your backend, you can connect to the XRP Ledger using the [HTTP APIs](../../references/http-websocket-apis/index.md). + + + +## 4. Set Up Your Accounts + +With your chosen client library, you can create and fund the accounts your integration will use to submit transactions and hold balances. On the XRP Ledger, a few things about accounts work differently: +- An account doesn't exist until it's funded. You can generate a [key pair](../../concepts/accounts/cryptographic-keys.md) offline, but the address isn't active on the ledger until it receives the [base reserve](../../concepts/accounts/reserves.md). +- Its reserve has two parts: a fixed [base reserve](../../concepts/accounts/reserves.md) every account holds, plus an owner reserve that adds a set amount for each object it owns (e.g., a trust line, offer, or escrow). You can't spend below the two combined, which doubles as one of the ledger's anti-spam mechanisms. +- To hold or receive an issued currency or [stablecoin](../../concepts/tokens/fungible-tokens/stablecoins/index.md), an account first needs a [trust line](../../concepts/tokens/fungible-tokens/trust-line-tokens.md) to the issuer, and each trust line adds to the account's reserve. + +You can fund accounts on Testnet and Devnet using the [faucet](/resources/dev-tools/xrp-faucets). On [Mainnet](../../concepts/networks-and-servers/parallel-networks.md), you fund an account by sending it XRP from an exchange or an already-funded account. See [Accounts](../../concepts/accounts/index.md) for more on account creation and configuration. + +On Testnet, creating and funding an account is a single call: + +{% tabs %} +{% tab label="JavaScript" %} +{% code-snippet file="/_code-samples/multisigning/js/set-up-multi-signing.js" language="js" from="Funding new wallet" before="// Generate key pairs" /%} +{% /tab %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/multisigning/py/set-up-multi-signing.py" language="py" from="Funding new wallet" before="# Generate key pairs" /%} +{% /tab %} +{% /tabs %} + +{% partial file="/docs/_snippets/testnet-seed-warning.md" /%} + +Many operators separate roles across multiple accounts: a treasury for reserves and balances, operating accounts for submission, and a [SignerList](../../concepts/accounts/multi-signing.md) where an account needs multi-party control. Since these accounts hold real funds and on-ledger actions are irreversible, give them a key strategy from the start: generate each master key pair with [offline account setup](../../tutorials/best-practices/key-management/offline-account-setup.md), operate through a [regular key](../../tutorials/best-practices/key-management/assign-a-regular-key-pair.md), and [disable the master key](../../tutorials/best-practices/key-management/disable-master-key-pair.md) so a compromised operating key can be rotated without losing the account. See [Send a Multi-Signed Transaction](../../tutorials/best-practices/key-management/send-a-multi-signed-transaction.md) to set up multi-signing. + +If you operate with a custodian, they create and manage these accounts and their keys. The steps outlined here only apply when you hold your own keys. + +## 5. Build Your Integration + +Now that your accounts are funded, you can port your business logic into native transaction types. Every transaction type follows the same lifecycle: construction, autofill, signing, submission, and verification. Once you understand this pattern, the rest of your integration will follow it. + +Some common Web2 payments concepts work like this on the XRP Ledger: + +| Web2 Concept | XRP Ledger Field | +| --- | --- | +| Nonce | [`Sequence`](../../references/protocol/data-types/basic-data-types.md#account-sequence) numbers order transactions serially per account. | +| Gas | [`Fee`](../../concepts/transactions/transaction-cost.md) is a low base cost that rises with network load. | +| Mempool | [`LastLedgerSequence`](../../concepts/transactions/reliable-transaction-submission.md) bounds how long a transaction stays pending; the XRP Ledger has no shared mempool. | +| Retries | [`Sequence`](../../references/protocol/data-types/basic-data-types.md#account-sequence) makes a resubmitted transaction apply at most once. | + +For the guarantees behind these, see [Reliable Transaction Submission](../../concepts/transactions/reliable-transaction-submission.md). + +### Send a Payment + +{% tabs %} +{% tab label="JavaScript" %} +{% code-snippet file="/_code-samples/send-xrp/js/send-xrp.js" language="js" from="// Prepare transaction" before="// End of main()" /%} +{% /tab %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/send-xrp/py/send-xrp.py" language="py" from="# Prepare transaction" /%} +{% /tab %} +{% /tabs %} + +{% partial file="/docs/_snippets/testnet-seed-warning.md" /%} + +### Convert Across Currencies + +The previous example sends XRP, but [cross-currency payments](../../concepts/payment-types/cross-currency-payments.md) follow the same lifecycle, with two differences: +- `Amount` names a different currency and issuer than the source spends. +- `SendMax` caps the source spend. + +Both the sending and receiving accounts need a [trust line](../../concepts/tokens/fungible-tokens/trust-line-tokens.md) to the issuer of any non-XRP currency they handle. Issuers can restrict who holds their currency with [authorized trust lines](../../concepts/tokens/fungible-tokens/authorized-trust-lines.md). + +The ledger automatically routes through the [DEX](../../concepts/tokens/decentralized-exchange/index.md) and [bridges through XRP](../../concepts/tokens/decentralized-exchange/autobridging.md) when that's cheaper. This collapses any external routing and conversion layer into just two fields, so there is no router to integrate. + +For regulated flows, a cross-currency payment can name a [domain ID](../../concepts/tokens/decentralized-exchange/permissioned-dexes.md) so it consumes only offers from the matching permissioned DEX, and authorized trust lines keep an unauthorized account from receiving your issued balance in the first place. + +On-chain conversion draws on both the order book and the [AMM](../../concepts/tokens/decentralized-exchange/automated-market-makers.md), and [pathfinding](../../concepts/tokens/fungible-tokens/paths.md) estimates the route rather than guaranteeing it. Bound each conversion: `SendMax` caps what the source spends, and a `DeliverMin` with the [`tfPartialPayment`](../../concepts/payment-types/partial-payments.md) flag floors what the destination receives. + +The following example spends XRP and delivers USD to the destination, with both bounds set: + +{% tabs %} +{% tab label="JavaScript" %} +{% code-snippet file="/_code-samples/cross-currency-payment/js/sendCrossCurrency.js" language="js" from="Prepare cross-currency Payment" before="End cross-currency payment" /%} +{% /tab %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/cross-currency-payment/py/send_cross_currency.py" language="py" from="Prepare cross-currency Payment" before="End cross-currency payment" /%} +{% /tab %} +{% /tabs %} + +### Receive and Credit + +Sending is only half of the integration. Your stack also has to detect payments arriving in your accounts and credit the correct user. Subscribe to your accounts with [transaction streams](../../references/http-websocket-apis/public-api-methods/subscription-methods/subscribe.md#transaction-streams) to see payments as they validate. Before you rely on this in production, see [Robustly Monitoring for Payments](../../concepts/payment-types/robustly-monitoring-for-payments.md). + +{% admonition type="warning" name="Caution" %} +Handle two XRP Ledger specifics before crediting anyone: + +- **Credit the amount that actually arrived.** A [partial payment](../../concepts/payment-types/partial-payments.md) can deliver less than the `Amount` field shows. Read the `delivered_amount` metadata field, never `Amount`, or you expose the [partial payments exploit](../../concepts/payment-types/partial-payments.md#partial-payments-exploit). +- **Attribute pooled funds with destination tags.** When many users share one treasury or operating account, require [destination tags](../../concepts/transactions/source-and-destination-tags.md) with [`RequireDest`](../../tutorials/compliance-features/require-destination-tags.md) so each payment maps to the right user. +{% /admonition %} + +## 6. Test on Testnet or Devnet + +Before going live, complete your end-to-end testing on a public test network: +- [Testnet](../../concepts/networks-and-servers/parallel-networks.md): Mirrors Mainnet's amendment status and behavior. Primary environment for pre-launch validation. +- [Devnet](../../concepts/networks-and-servers/parallel-networks.md): Preview version for testing pre-release features or amendments that are not yet on Mainnet. + +Neither uses real money, but both test networks are centralized and can be reset at any time. Don't rely on persistence or treat them like stable environments. + +Then exercise the XRP Ledger-specific behaviors that can lose or misattribute funds, such as crediting `delivered_amount` on partial payments, attributing pooled funds with destination tags, enforcing reserve and trust line limits, holding `SendMax`/`DeliverMin` under thin liquidity, and clearing submissions when the fee rises under load. + +A submitted transaction isn't final until it's in a validated ledger, and being included is not the same as succeeding. Only `tesSUCCESS` and `tec` results are final. `tesSUCCESS` indicates that the transaction succeeded, while a `tec` still consumed the `Fee`, so treat it as a failure rather than retrying. + +You can resubmit a transaction if it expires without validation. If you use `submitAndWait` and `autofill` (as in the previous step), the library waits for the validated result and sets the bounding fields for you. See [Transaction Results](../../references/protocol/transactions/transaction-results/index.md) and [Reliable Transaction Submission](../../concepts/transactions/reliable-transaction-submission.md). + +## 7. Pilot a Live Corridor and Launch + +After testing on Testnet or Devnet, run a live pilot with real traffic through one narrow corridor (or a single partner), and confirm reconciliation matches your expectations before you scale. + +To do so: + +1. Switch your endpoint from a test network to Mainnet. +2. Use real, funded Mainnet accounts. +3. Send real transactions through the same lifecycle (see [Build Your Integration](#5-build-your-integration)) and verify each reached a validated ledger. +4. Verify on-ledger results against your internal records. Query transaction history with [`account_tx`](../../references/http-websocket-apis/public-api-methods/account-methods/account_tx.md) (or via your Clio server) for reconciliation, and subscribe to [transaction streams](../../references/http-websocket-apis/public-api-methods/subscription-methods/subscribe.md#transaction-streams) for real-time monitoring instead of polling. At volume, page through `account_tx` with the returned `marker`, backfill with a bounded `account_tx` query if a stream disconnects, and make reconciliation idempotent so replaying a ledger range never double-counts. See [Robustly Monitoring for Payments](../../concepts/payment-types/robustly-monitoring-for-payments.md). +5. Enforce any KYC or travel-rule obligations for the corridor. See [Require Destination Tags](../../tutorials/compliance-features/require-destination-tags.md), and use [credentials](../../tutorials/compliance-features/manage-credentials.md) to attest a holder's verified status on-ledger. +6. Widen scope once verified. If reconciliation fails, delay the cutover and keep your existing rail live as the fallback. + +Reconciliation is cleaner than on many networks. Once a transaction is in a validated ledger it's immutable and can't be reversed, so there's no probabilistic-settlement tail to account for. See [Finality of Results](../../concepts/transactions/finality-of-results/index.md) and [Consensus](../../concepts/consensus-protocol/index.md). That makes the verify step in your pilot straightforward. + +How you cut over depends on your custody model: +- Custodial setups switch in a single window: snapshot your balances, fund the accounts on the XRP Ledger, and flip the payment engine. +- Self-custodial flows rely on users moving their own balances, so plan for a longer arc. User communication is usually the slowest piece. + +Your payments stack now runs on native XRP Ledger primitives, giving you fewer components to build, secure, and maintain than the one you migrated from. + +## See Also + +- **Concepts:** + - [Cross-Currency Payments](../../concepts/payment-types/cross-currency-payments.md) + - [Decentralized Exchange](../../concepts/tokens/decentralized-exchange/index.md) + - [Multi-Signing](../../concepts/accounts/multi-signing.md) + - [Stablecoins](../../concepts/tokens/fungible-tokens/stablecoins/index.md) +- **Tutorials:** + - [Get Started with the XRP Ledger](../../tutorials/get-started/get-started-javascript.md) + - [Send a Multi-Signed Transaction](../../tutorials/best-practices/key-management/send-a-multi-signed-transaction.md) +- **References:** + - [Transaction Types](../../references/protocol/transactions/types/index.md) + - [Client Libraries](../../references/client-libraries.md) \ No newline at end of file diff --git a/sidebars.yaml b/sidebars.yaml index b79db5d9f7d..f965d2b4a1f 100644 --- a/sidebars.yaml +++ b/sidebars.yaml @@ -21,6 +21,7 @@ labelTranslationKey: sidebar.docs.use-cases.payments expanded: false items: + - page: docs/use-cases/payments/migrate-a-payments-or-fx-stack-to-the-xrp-ledger.md - page: docs/use-cases/payments/peer-to-peer-payments-uc.md - page: docs/use-cases/payments/restricting-deposits-uc.md - page: docs/use-cases/payments/smart-contracts-uc.md