# Auto Minting and Bridging FXRP

> Mint and bridge FXRP using Flare Smart Accounts

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/fxrp/oft/fxrp-automint

## Overview[​](#overview "Direct link to Overview")

In this guide, you will learn how to mint FXRP and bridge it cross-chain from Flare Testnet Coston2 to Sepolia using [Flare Smart Accounts](/smart-accounts/overview) and [LayerZero's OFT](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) (Omnichain Fungible Token) protocol.

This guide demonstrates an end-to-end workflow that allows XRPL users to:

1.  **Mint FXRP** - Convert native XRP from the XRP Ledger into FXRP tokens on Flare, controlled entirely via XRPL transactions.
2.  **Bridge FXRP cross-chain** - Transfer the minted FXRP to another EVM chain (Sepolia) using LayerZero, triggered by a single XRPL payment.

**Key technologies:**

-   [Flare Smart Accounts](/smart-accounts/overview) - Account abstraction enabling XRPL users to execute actions on Flare without holding FLR tokens.
-   **Custom Instructions** - Register arbitrary contract calls that can be triggered via XRPL payments.
-   [FAsset System](/fassets/overview) for tokenizing non-smart contract assets (XRP to FXRP).
-   [LayerZero OFT](https://docs.layerzero.network/v2/developers/evm/oft/quickstart) for cross-chain token transfers.
-   [@flarenetwork/smart-accounts-encoder](https://www.npmjs.com/package/@flarenetwork/smart-accounts-encoder) library for encoding FAsset instructions.

Clone the [Flare Hardhat Starter](https://github.com/flare-foundation/flare-hardhat-starter) to follow along.

## How Smart Accounts Enable This Workflow[​](#how-smart-accounts-enable-this-workflow "Direct link to How Smart Accounts Enable This Workflow")

[Flare Smart Accounts](/smart-accounts/overview) allow XRPL users to perform actions on the Flare chain without owning any FLR tokens. Each XRPL address is assigned a unique **personal account** (smart contract wallet) on Flare, which only that XRPL address can control through `Payment` transactions on the XRP Ledger.

This script leverages two types of Smart Account instructions:

### 1\. FAsset Collateral Reservation Instruction[​](#1-fasset-collateral-reservation-instruction "Direct link to 1. FAsset Collateral Reservation Instruction")

The [`FXRPCollateralReservationInstruction`](/smart-accounts/fasset-instructions#00-collateral-reservation) is a predefined instruction type that initiates the FAsset minting process. When sent via an XRPL payment memo:

1.  The operator monitors the XRPL payment and relays it to Flare.
2.  The `MasterAccountController` contract reserves collateral from the selected agent.
3.  After the user sends XRP to the agent's underlying address, FXRP is minted to their personal account.

### 2\. Custom Instruction for Atomic Bridging[​](#2-custom-instruction-for-atomic-bridging "Direct link to 2. Custom Instruction for Atomic Bridging")

Custom instructions allow registering arbitrary contract calls that execute atomically. This script registers an **atomic batch** containing:

1.  **Approve**: Grant the OFT Adapter permission to spend FXRP tokens.
2.  **Send**: Execute the LayerZero cross-chain transfer.

Both actions execute in a single transaction when triggered by the XRPL payment, ensuring the bridge cannot fail due to missing approval.

## Flow Diagram[​](#flow-diagram "Direct link to Flow Diagram")

```
                          XRPL USER WORKFLOW------------------------------------------------------------------------                                    |        ----------------------------|----------------------------        |                           |                           |        v                           |                           |+-------------------+               |                           ||   1. Register     |               |                           ||   Custom Bridge   |               |                           ||   Instruction     |               |                           ||   (Flare EOA)     |               |                           |+--------+----------+               |                           |         |                          |                           |         | Returns instruction hash |                           |         v                          |                           |+-------------------+               |                           ||  2. Check Smart   |               |                           ||     Account       |               |                           ||     Balance       |               |                           |+--------+----------+               |                           |         |                          |                           |         | Needs FXRP?              |                           |         v                          v                           |+-------------------+     +-------------------+                 ||  3a. Mint FXRP    |     |  3b. Skip Mint    |                 ||  (If needed)      |     |  (Has balance)    |                 |+--------+----------+     +--------+----------+                 |         |                         |                            |         +-----------+-------------+                            |                     |                                          |                     v                                          |         +-------------------+                                  |         |  4. Execute       |<---------------------------------+         |     Bridge        |         |  (XRPL Payment)   |         +--------+----------+                  |                  v                         CROSS-CHAIN FLOW------------------------------------------------------------------------+-------------------+          +-------------------+          +-------------------+|    XRP Ledger     |          |   Flare Coston2   |          |     Sepolia       ||                   |          |                   |          |                   ||  XRPL Payment     |--------->|  MasterAccount    |          |                   ||  with Memo        |  FDC     |  Controller       |          |                   ||                   |  Proof   |       |           |          |                   ||                   |          |       v           |          |                   ||                   |          |  Personal Account |          |                   ||                   |          |       |           |          |                   ||                   |          |       v           |          |                   ||                   |          |  Atomic Batch:    |          |                   ||                   |          |  +-------------+  |          |                   ||                   |          |  | 1. Approve  |  |          |                   ||                   |          |  | 2. LZ Send  |--|--------->|  FXRP OFT        ||                   |          |  +-------------+  |LayerZero |  Received        ||                   |          |                   |          |                   |+-------------------+          +-------------------+          +-------------------+
```

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

-   **XRPL Testnet Account**: An XRP Ledger testnet wallet with XRP for payments. Get testnet XRP from the [XRP Testnet Faucet](https://xrpl.org/resources/dev-tools/xrp-faucets).
-   **Flare Testnet Account**: An EVM wallet with C2FLR for gas fees. Get C2FLR from the [Flare Testnet Faucet](https://faucet.flare.network/coston2).
-   **Environment Setup**: Private keys configured in Hardhat.

## Configuration[​](#configuration "Direct link to Configuration")

Edit the `CONFIG` object in the script to customize the bridge parameters:

```
const CONFIG = {  MASTER_ACCOUNT_CONTROLLER: "0xa7bc2aC84DB618fde9fa4892D1166fFf75D36FA6",  COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",  XRPL_RPC: "wss://s.altnet.rippletest.net:51233",  SEPOLIA_EID: EndpointId.SEPOLIA_V2_TESTNET,  EXECUTOR_GAS: 400_000,  BRIDGE_LOTS: 1, // Number of lots to bridge  AUTO_MINT_IF_NEEDED: true, // Automatically mint if insufficient balance  MINT_LOTS: 1, // Number of lots to mint if needed};
```

Parameter

Description

`MASTER_ACCOUNT_CONTROLLER`

Address of the MasterAccountController contract on Coston2

`COSTON2_OFT_ADAPTER`

Address of the FXRP OFT Adapter for LayerZero bridging

`XRPL_RPC`

WebSocket URL for XRPL Testnet

`SEPOLIA_EID`

LayerZero Endpoint ID for the destination chain

`EXECUTOR_GAS`

Gas limit for the LayerZero executor on the destination chain

`BRIDGE_LOTS`

Number of FXRP lots to bridge (1 lot = 10 FXRP)

`AUTO_MINT_IF_NEEDED`

Whether to automatically mint FXRP if the personal account has insufficient balance

`MINT_LOTS`

Number of lots to mint if auto-minting is triggered

## How to Run[​](#how-to-run "Direct link to How to Run")

1.  **Install Dependencies**:
    
    ```
    yarn install
    ```
    
2.  **Configure Environment**:
    
    ```
    # .env fileCOSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpcDEPLOYER_PRIVATE_KEY=your_flare_private_key_hereXRPL_SECRET=your_xrpl_wallet_secret_here
    ```
    
3.  **Run the Script**:
    
    ```
    yarn hardhat run scripts/smartAccounts/bridgeViaSmartAccount.ts --network coston2
    ```
    

## Script Walkthrough[​](#script-walkthrough "Direct link to Script Walkthrough")

### Step 1: Register the Bridge Instruction[​](#step-1-register-the-bridge-instruction "Direct link to Step 1: Register the Bridge Instruction")

The script first registers a custom instruction with the `MasterAccountController`. This instruction bundles two contract calls into an atomic batch:

```
// 1. Prepare APPROVE Callconst instructionApprove: CustomInstruction = {  targetContract: fxrpAddress,  value: 0n,  data: approveCallData, // ERC20 approve(spender, amount)};// 2. Prepare SEND Callconst instructionBridge: CustomInstruction = {  targetContract: CONFIG.COSTON2_OFT_ADAPTER,  value: nativeFee, // LayerZero fee in native tokens  data: sendCallData, // OFT send() with LayerZero options};// 3. Register the atomic batchconst atomicInstruction = [instructionApprove, instructionBridge];await masterController.methods  .registerCustomInstruction(atomicInstruction)  .send({ from: accounts[0] });
```

The registration returns an **instruction hash** that will be used as the XRPL payment memo to trigger execution. The memo format for custom instructions is:

-   First byte: `99` (custom instruction identifier)
-   Remaining 31 bytes: instruction hash (padded)

### Step 2: Check Smart Account Balance[​](#step-2-check-smart-account-balance "Direct link to Step 2: Check Smart Account Balance")

Before bridging, the script checks if the user's personal account has sufficient:

1.  **FXRP balance** - Enough tokens to bridge
2.  **Native balance (C2FLR)** - Enough gas to pay for the LayerZero fee

```
const personalAccountAddr = await masterController.methods  .getPersonalAccount(xrplAddress)  .call();const fxrpBalance = await ftestxrp.balanceOf(personalAccountAddr);const nativeBalance = await web3.eth.getBalance(personalAccountAddr);
```

If the personal account doesn't exist yet, it will be created automatically when the first instruction is executed.

### Step 3: Fund Gas (If Needed)[​](#step-3-fund-gas-if-needed "Direct link to Step 3: Fund Gas (If Needed)")

If the personal account lacks sufficient native tokens for the LayerZero fee, the script funds it from the Flare EOA:

```
if (status.needsGas && status.hasAccount) {  await web3.eth.sendTransaction({    from: accounts[0],    to: status.personalAccountAddr,    value: (requiredGas - status.currentNative + BigInt(1e17)).toString(),  });}
```

### Step 4: Mint FXRP (If Needed)[​](#step-4-mint-fxrp-if-needed "Direct link to Step 4: Mint FXRP (If Needed)")

If the personal account has insufficient FXRP, the script initiates the [FAsset minting process](/fassets/minting) using the Smart Accounts system:

```
import { FXRPCollateralReservationInstruction } from "@flarenetwork/smart-accounts-encoder";// Encode the collateral reservation instructionconst reservationInstruction = new FXRPCollateralReservationInstruction({  walletId: 0,  value: lots, // Number of lots to mint  agentVaultId: agentIndex, // Selected agent's index});// Send XRPL payment with the instruction memoconst instructionMemo = reservationInstruction.encode().slice(2);await sendXrplMemoPayment(xrplWallet, operatorAddress, "1", instructionMemo);
```

The minting process involves:

1.  **Reservation Trigger**: Send an XRPL payment to the operator with the encoded instruction.
2.  **Wait for Reservation**: The operator processes the payment and reserves collateral on Flare.
3.  **Send Underlying Collateral**: Send XRP to the agent's underlying address with the payment reference.
4.  **Mint Execution**: The operator executes the mint, depositing FXRP to the personal account.

### Step 5: Execute the Bridge[​](#step-5-execute-the-bridge "Direct link to Step 5: Execute the Bridge")

Finally, trigger the bridge by sending an XRPL payment with the custom instruction memo:

```
await sendXrplMemoPayment(xrplWallet, operatorAddress, "0.1", bridgeMemo);
```

When the operator relays this payment to Flare:

1.  The `MasterAccountController` looks up the registered instruction by its hash.
2.  The personal account executes the atomic batch:
    -   Approves the OFT Adapter to spend FXRP
    -   Calls the OFT Adapter's `send()` function
3.  LayerZero delivers the tokens to Sepolia.

## Expected Output[​](#expected-output "Direct link to Expected Output")

```
Flare EOA: 0x742d35Cc6634C0532925a3b844Bc454e4438f44eXRPL Wallet: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyThBridging 1 lot(s) = 10.0 FXRP=== Step 1: Registering Atomic Bridge Instruction ===LayerZero Fee: 0.001234 C2FLR required in personal accountSubmitting registration tx...Instruction Registered.Final XRPL Memo: 99000000...abc123=== Checking Smart Account Balance ===Personal Account: 0x123...FXRP Balance: 15.0C2FLR Balance: 0.5Sufficient FXRP balance found. Skipping mint.=== Bridging to Sepolia via Custom Instruction ===Sending Bridge Trigger on XRPL...Sending 0.1 XRP to rOperator... with Memo 99000000...abc123Tx Hash: ABC123...Bridge Request Sent! (Asynchronous execution on Flare will follow)
```

<details>
<summary>View bridgeViaSmartAccount.ts source code</summary>

View `bridgeViaSmartAccount.ts` source code

scripts/smartAccounts/bridgeViaSmartAccount.ts

```
/** * Usage: * yarn hardhat run scripts/smartAccounts/bridgeViaSmartAccount.ts --network coston2 */import { web3, artifacts } from "hardhat";import { formatUnits } from "ethers";import { Options } from "@layerzerolabs/lz-v2-utilities";import { EndpointId } from "@layerzerolabs/lz-definitions";import { Client, Wallet as XrplWallet, xrpToDrops } from "xrpl";import type { Payment } from "xrpl";import { FXRPCollateralReservationInstruction } from "@flarenetwork/smart-accounts-encoder";import { getAssetManagerFXRP } from "../utils/getters";import { sleep } from "../utils/core";import type {  IAssetManagerInstance,  IERC20Instance,} from "../../typechain-types";import * as fs from "fs";import * as path from "path";const IERC20 = artifacts.require("IERC20");// ABIsconst MASTER_ACCOUNT_CONTROLLER_ABI = JSON.parse(  fs.readFileSync(    path.join(__dirname, "../abi/MasterAccountController.json"),    "utf-8",  ),).abi;const FASSET_OFT_ADAPTER_ABI = JSON.parse(  fs.readFileSync(    path.join(__dirname, "../abi/FAssetOFTAdapter.json"),    "utf-8",  ),).abi;type CustomInstruction = {  targetContract: string;  value: bigint;  data: string;};// Configurationconst CONFIG = {  MASTER_ACCOUNT_CONTROLLER: "0xa7bc2aC84DB618fde9fa4892D1166fFf75D36FA6",  COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",  XRPL_RPC: "wss://s.altnet.rippletest.net:51233",  SEPOLIA_EID: EndpointId.SEPOLIA_V2_TESTNET,  EXECUTOR_GAS: 400_000,  BRIDGE_LOTS: 1, // Number of lots to bridge  AUTO_MINT_IF_NEEDED: true,  MINT_LOTS: 1,} as const;/** * Get the FXRP token address and calculate bridge amount from lots * @see https://dev.flare.network/fassets/developer-guides/fassets-fxrp-address */async function getAssetManagerInfo(lots: number) {  const assetManager = await getAssetManagerFXRP();  const fxrpAddress = await assetManager.fAsset();  const lotSizeBN = await assetManager.lotSize();  const lotSize = BigInt(lotSizeBN.toString());  const amountToBridge = lotSize * BigInt(lots);  return {    fxrpAddress,    amountToBridge,  };}async function getWallets() {  const accounts = await web3.eth.getAccounts();  const signerAddress = accounts[0];  const xrplSecret = process.env.XRPL_SECRET;  if (!xrplSecret) throw new Error("XRPL_SECRET not set in .env");  const xrplWallet = XrplWallet.fromSeed(xrplSecret);  console.log(`Flare EOA: ${signerAddress}`);  console.log(`XRPL Wallet: ${xrplWallet.address}`);  return { signerAddress, xrplWallet };}function getMasterController() {  return new web3.eth.Contract(    MASTER_ACCOUNT_CONTROLLER_ABI,    CONFIG.MASTER_ACCOUNT_CONTROLLER,  );}/** * Step 1: Register the Bridge Instruction on Flare * Creates an ATOMIC BATCH: [Approve Token, Send Token] */async function registerBridgeInstruction(  recipientAddress: string,  amountToBridge: bigint,  fxrpAddress: string,) {  console.log("\n=== Step 1: Registering Atomic Bridge Instruction ===");  const oftAdapter = new web3.eth.Contract(    FASSET_OFT_ADAPTER_ABI,    CONFIG.COSTON2_OFT_ADAPTER,  );  const ftestxrp = new web3.eth.Contract(IERC20.abi, fxrpAddress);  // 1. Prepare APPROVE Call (Personal Account -> OFT Adapter)  const approveCallData = ftestxrp.methods    .approve(CONFIG.COSTON2_OFT_ADAPTER, amountToBridge.toString())    .encodeABI();  const instructionApprove: CustomInstruction = {    targetContract: fxrpAddress,    value: 0n,    data: approveCallData,  };  // 2. Prepare SEND Call (Personal Account -> LayerZero)  const options = Options.newOptions().addExecutorLzReceiveOption(    CONFIG.EXECUTOR_GAS,    0,  );  const sendParam = {    dstEid: CONFIG.SEPOLIA_EID,    to: web3.utils.padLeft(recipientAddress, 64),    amountLD: amountToBridge.toString(),    minAmountLD: amountToBridge.toString(),    extraOptions: options.toHex(),    composeMsg: "0x",    oftCmd: "0x",  };  const quoteResult = await oftAdapter.methods    .quoteSend(sendParam, false)    .call();  const nativeFee = BigInt(quoteResult.nativeFee);  console.log(    `LayerZero Fee: ${formatUnits(nativeFee, 18)} C2FLR required in personal account`,  );  const feeStruct = { nativeFee: nativeFee.toString(), lzTokenFee: "0" };  const sendCallData = oftAdapter.methods    .send(sendParam, feeStruct, recipientAddress)    .encodeABI();  const instructionBridge: CustomInstruction = {    targetContract: CONFIG.COSTON2_OFT_ADAPTER,    value: nativeFee, // Gas needed for this specific step    data: sendCallData,  };  // 3. Bundle & Register  const atomicInstruction: CustomInstruction[] = [    instructionApprove,    instructionBridge,  ];  const masterController = getMasterController();  const accounts = await web3.eth.getAccounts();  console.log("Submitting registration tx...");  // Note: In a real app, check if this exact hash is already registered to save gas  await masterController.methods    .registerCustomInstruction(atomicInstruction)    .send({ from: accounts[0] });  const encodedInstructionBN = await masterController.methods    .encodeCustomInstruction(atomicInstruction)    .call();  let instructionHash = BigInt(encodedInstructionBN).toString(16);  if (instructionHash.length % 2 !== 0) instructionHash = "0" + instructionHash;  console.log("✅ Instruction Registered.");  const finalMemo = "99" + instructionHash.padStart(60, "0");  console.log("Final XRPL Memo:", finalMemo);  return { memo: finalMemo, requiredGas: nativeFee };}async function sendXrplMemoPayment(  xrplWallet: XrplWallet,  destination: string,  amountXrp: string,  memoHex: string,) {  const client = new Client(CONFIG.XRPL_RPC);  await client.connect();  try {    const payment: Payment = {      TransactionType: "Payment",      Account: xrplWallet.address,      Destination: destination,      Amount: xrpToDrops(amountXrp),      Memos: [{ Memo: { MemoData: memoHex.toUpperCase() } }],    };    console.log(      `Sending ${amountXrp} XRP to ${destination} with Memo ${memoHex}...`,    );    const prepared = await client.autofill(payment);    const signed = xrplWallet.sign(prepared);    const result = await client.submitAndWait(signed.tx_blob);    if (      result.result.meta &&      typeof result.result.meta === "object" &&      result.result.meta.TransactionResult !== "tesSUCCESS"    ) {      throw new Error(        `XRPL Payment Failed: ${result.result.meta.TransactionResult}`,      );    }    console.log(`Tx Hash: ${result.result.hash}`);  } finally {    await client.disconnect();  }}async function checkPersonalAccount(  xrplAddress: string,  requiredAmountFXRP: bigint,  requiredGas: bigint,  fxrpAddress: string,) {  console.log("\n=== Checking Smart Account Balance ===");  const masterController = getMasterController();  const personalAccountAddr = await masterController.methods    .getPersonalAccount(xrplAddress)    .call();  const hasAccount =    personalAccountAddr !== "0x0000000000000000000000000000000000000000";  let fxrpBalance = 0n;  let nativeBalance = 0n;  if (hasAccount) {    const ftestxrp: IERC20Instance = await IERC20.at(fxrpAddress);    fxrpBalance = BigInt(await ftestxrp.balanceOf(personalAccountAddr));    nativeBalance = BigInt(await web3.eth.getBalance(personalAccountAddr));    console.log(`Personal Account: ${personalAccountAddr}`);    console.log(`FXRP Balance: ${formatUnits(fxrpBalance, 18)}`);    console.log(`C2FLR Balance: ${formatUnits(nativeBalance, 18)}`);  } else {    console.log("Personal Account: Not created yet");  }  return {    personalAccountAddr,    hasAccount,    needsMint: fxrpBalance < requiredAmountFXRP,    needsGas: nativeBalance < requiredGas,    currentNative: nativeBalance,  };}async function waitForReservationEvent(  assetManager: IAssetManagerInstance,  agentVault: string,  startBlock: number,) {  console.log("⏳ Waiting for Operator to Execute Reservation...");  let currentFrom = startBlock;  const MAX_BLOCK_RANGE = 25;  const MAX_DURATION = 15 * 60 * 1000;  const startTime = Date.now();  while (Date.now() - startTime < MAX_DURATION) {    const latest = await web3.eth.getBlockNumber();    while (currentFrom <= latest) {      const currentTo = Math.min(currentFrom + MAX_BLOCK_RANGE, latest);      const events = await assetManager.getPastEvents("CollateralReserved", {        fromBlock: currentFrom,        toBlock: currentTo,        filter: { agentVault: agentVault },      });      if (events.length > 0) {        const evt = events[events.length - 1];        console.log("\n✅ Event Detected in block", evt.blockNumber);        return {          valueUBA: BigInt(evt.returnValues.valueUBA),          paymentReference: evt.returnValues.paymentReference,        };      }      currentFrom = currentTo + 1;    }    process.stdout.write(".");    await sleep(5000);  }  throw new Error("Timeout waiting for reservation event.");}async function mintFXRP(xrplWallet: XrplWallet, lots: number) {  console.log(`\n=== Starting Mint for ${lots} Lot(s) ===`);  const assetManager = await getAssetManagerFXRP();  const masterController = getMasterController();  const operatorAddress = await masterController.methods    .xrplProviderWallet()    .call();  const agents = await assetManager.getAvailableAgentsDetailedList(0, 20);  // Note: This is a proof of concept. In production, you can select your own agent.  const agentIndex = agents._agents.findIndex(    (a) => BigInt(a.freeCollateralLots) >= BigInt(lots),  );  if (agentIndex === -1) throw new Error("No agents available");  const agent = agents._agents[agentIndex];  console.log(`Selected Agent: ${agent.agentVault} (index: ${agentIndex})`);  const agentInfo = await assetManager.getAgentInfo(agent.agentVault);  const agentXrplAddress = agentInfo.underlyingAddressString;  // Encode mint instruction using smart-accounts-encoder library  const reservationInstruction = new FXRPCollateralReservationInstruction({    walletId: 0,    value: lots,    agentVaultId: agentIndex,  });  const instructionMemo = reservationInstruction.encode().slice(2); // Remove '0x' prefix for XRPL memo  const currentBlock = await web3.eth.getBlockNumber();  console.log(`1. Sending Reservation Trigger...`);  await sendXrplMemoPayment(xrplWallet, operatorAddress, "1", instructionMemo);  const { valueUBA, paymentReference } = await waitForReservationEvent(    assetManager,    agent.agentVault,    currentBlock,  );  const xrpAmount = Number(valueUBA) / 1_000_000;  console.log(`\n✅ Reservation Confirmed.`);  console.log(`2. Sending Underlying Collateral to Agent...`);  const refClean = paymentReference.replace("0x", "");  await sendXrplMemoPayment(    xrplWallet,    agentXrplAddress,    xrpAmount.toString(),    refClean,  );  console.log("⏳ Waiting for FXRP Mint Execution (60s)...");  await sleep(60000);}async function executeBridge(xrplWallet: XrplWallet, bridgeMemo: string) {  console.log("\n=== Bridging to Sepolia via Custom Instruction ===");  const masterController = getMasterController();  const operatorAddress = await masterController.methods    .xrplProviderWallet()    .call();  console.log("Sending Bridge Trigger on XRPL...");  await sendXrplMemoPayment(xrplWallet, operatorAddress, "0.1", bridgeMemo);  console.log(    "\n✅ Bridge Request Sent! (Asynchronous execution on Flare will follow)",  );}/** * Main Flow */async function main() {  const { signerAddress, xrplWallet } = await getWallets();  // Get FXRP address and calculate bridge amount from lots  const { fxrpAddress, amountToBridge } = await getAssetManagerInfo(    CONFIG.BRIDGE_LOTS,  );  console.log(    `\nBridging ${CONFIG.BRIDGE_LOTS} lot(s) = ${formatUnits(amountToBridge, 6)} FXRP`,  );  // 1. Register custom instruction  const { memo: bridgeMemo, requiredGas } = await registerBridgeInstruction(    signerAddress,    amountToBridge,    fxrpAddress,  );  // 2. Check State  const status = await checkPersonalAccount(    xrplWallet.address,    amountToBridge,    requiredGas,    fxrpAddress,  );  // 3. Fund Gas  if (status.needsGas && status.hasAccount) {    console.log(`\n⚠️ Personal Account needs Native Gas! Sending C2FLR...`);    const accounts = await web3.eth.getAccounts();    await web3.eth.sendTransaction({      from: accounts[0],      to: status.personalAccountAddr,      value: (requiredGas - status.currentNative + BigInt(1e17)).toString(),    });    console.log("Gas funded.");  }  // 4. Mint (Skipped if balance exists!)  if (status.needsMint) {    if (!CONFIG.AUTO_MINT_IF_NEEDED) throw new Error("Insufficient Funds");    await mintFXRP(xrplWallet, CONFIG.MINT_LOTS);  } else {    console.log("✅ Sufficient FXRP balance found. Skipping mint.");  }  // 5. Execute Bridge  await executeBridge(xrplWallet, bridgeMemo);}main().catch((error) => {  console.error(error);  process.exit(1);});
```

</details>

## Code Breakdown[​](#code-breakdown "Direct link to Code Breakdown")

### Key Functions[​](#key-functions "Direct link to Key Functions")

#### `getAssetManagerInfo(lots)`[​](#getassetmanagerinfolots "Direct link to getassetmanagerinfolots")

Retrieves the FXRP token address and calculates the exact amount to bridge based on the lot size:

```
const assetManager = await getAssetManagerFXRP();const fxrpAddress = await assetManager.fAsset();const lotSize = BigInt(await assetManager.lotSize());const amountToBridge = lotSize * BigInt(lots);
```

#### `registerBridgeInstruction(recipientAddress, amountToBridge, fxrpAddress)`[​](#registerbridgeinstructionrecipientaddress-amounttobridge-fxrpaddress "Direct link to registerbridgeinstructionrecipientaddress-amounttobridge-fxrpaddress")

Creates and registers the atomic bridge instruction:

1.  Encodes the ERC20 `approve()` call for the OFT Adapter.
2.  Builds LayerZero send parameters with the destination chain and recipient.
3.  Quotes the LayerZero fee using `oftAdapter.quoteSend()`.
4.  Encodes the OFT `send()` call with the fee.
5.  Registers both calls as a single atomic instruction.

#### `sendXrplMemoPayment(xrplWallet, destination, amountXrp, memoHex)`[​](#sendxrplmemopaymentxrplwallet-destination-amountxrp-memohex "Direct link to sendxrplmemopaymentxrplwallet-destination-amountxrp-memohex")

Sends an XRP Ledger payment with an encoded memo:

```
const payment: Payment = {  TransactionType: "Payment",  Account: xrplWallet.address,  Destination: destination,  Amount: xrpToDrops(amountXrp),  Memos: [{ Memo: { MemoData: memoHex.toUpperCase() } }],};
```

#### `mintFXRP(xrplWallet, lots)`[​](#mintfxrpxrplwallet-lots "Direct link to mintfxrpxrplwallet-lots")

Executes the full FAsset minting flow:

1.  Selects an available agent with sufficient free collateral.
2.  Encodes and sends the collateral reservation instruction.
3.  Waits for the `CollateralReserved` event.
4.  Sends XRP collateral to the agent's underlying address.
5.  Waits for the mint to complete.

## Understanding the Instruction Encoding[​](#understanding-the-instruction-encoding "Direct link to Understanding the Instruction Encoding")

### FAsset Instructions[​](#fasset-instructions "Direct link to FAsset Instructions")

The `@flarenetwork/smart-accounts-encoder` library provides typed instruction builders. The collateral reservation instruction encodes to a 32-byte value:

Bytes

Content

0

Instruction ID (`00` for FXRP type, `00` for collateral reservation)

1

Wallet ID (typically `0`)

2-11

Value (number of lots)

12-13

Agent Vault ID

14-31

Reserved

### Custom Instructions[​](#custom-instructions "Direct link to Custom Instructions")

Custom instructions are identified by the first byte being `99`. The remaining 31 bytes contain the keccak256 hash of the encoded instruction array (right-shifted by 8 bits):

```
uint256(keccak256(abi.encode(_customInstruction))) >> 8;
```

## FAQ[​](#faq "Direct link to FAQ")

**Q: What's the minimum amount I can bridge?** A: Minimum is 1 [lot](/fassets/minting#lots) (10 FXRP for XRP).

**Q: How long does the minting process take?** A: Minting typically takes 1-2 minutes for the reservation, plus time for the underlying XRP payment to be confirmed and processed by the operator.

**Q: What if the bridge execution fails?** A: If the atomic instruction fails (e.g., insufficient balance), the entire transaction reverts. The FXRP remains in the personal account and can be retrieved or retried.

**Q: Can I bridge to chains other than Sepolia?** A: Yes, update the `SEPOLIA_EID` configuration to any LayerZero-supported destination. Use the [getOftPeers script](/fxrp/oft/fxrp-autoredeem#discovering-available-bridge-routes) to discover available routes.

**Q: Do I need FLR tokens to use this?** A: You need C2FLR (testnet FLR) to:

-   Register custom instructions (one-time gas cost)
-   Fund the personal account with native tokens for LayerZero fees

The actual bridging is triggered via XRPL payments and executed by the operator.

**Q: What is the operator's role?** A: The operator monitors XRPL payments to a designated address, obtains FDC proofs, and relays transactions to the `MasterAccountController` on Flare. This service enables XRPL users to trigger Flare actions without holding FLR.

**Q: Can I use this on mainnet?** A: This guide is for testnet. For mainnet deployment, update contract addresses, thoroughly test, and audit all code.

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

**Error: XRPL\_SECRET not set in .env**

-   Solution: Add your XRPL testnet wallet secret to the `.env` file.

**Error: No agents available**

-   Solution: No FAsset agents have sufficient free collateral. Try with fewer lots or wait for agents to free up capacity.

**Error: Timeout waiting for reservation event**

-   Solution: The operator may be delayed. Check that the XRPL payment was successful and retry.

**Error: Insufficient balance for bridging**

-   Solution: Ensure `AUTO_MINT_IF_NEEDED` is `true`, or manually mint FXRP to the personal account first.

**Error: LayerZero fee insufficient**

-   Solution: Fund the personal account with more C2FLR for the LayerZero cross-chain fee.

Next Steps

To continue your FAssets development journey, you can:

-   Learn how to [mint FXRP manually](/fassets/developer-guides/fassets-mint)
-   Understand how to [redeem FXRP](/fassets/developer-guides/fassets-redeem)
-   Explore [auto-redemption from Hyperliquid](/fxrp/oft/fxrp-autoredeem)
-   Read more about [Flare Smart Accounts](/smart-accounts/overview)
