# Cross-Chain Redeem to Tag

> Send FXRP from Sepolia back to Flare and auto-redeem to an XRPL address with a destination tag.

> 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/smart-accounts/guides/typescript-viem/cross-chain-redeem-to-tag-ts

This guide is a variant of the [Cross-Chain Redeem guide](/smart-accounts/guides/typescript-viem/cross-chain-redeem-ts) that redeems FXRP to an XRPL address with a [destination tag](https://xrpl.org/docs/concepts/transactions/source-and-destination-tags). Read the redeem guide first — only the differences are covered here.

info

The code in this guide is set up for the Coston2 testnet and the Sepolia testnet. Despite that, we will refer to the Flare-side network as Flare and its currency as FLR instead of Coston2 and C2FLR. Likewise, we will refer to the Sepolia testnet's currency as ETH instead of SETH.

## What's different[​](#whats-different "Direct link to What's different")

Three things change relative to the tagless redeem:

-   the compose message is a 6-field struct instead of a 3-tuple — it carries the destination tag and reserves space for an optional executor and executor fee.
-   a new environment variable `REDEMPTION_DESTINATION_TAG` controls the XRPL destination tag (defaulting to `72`).
-   the script waits for a `FAssetRedeemed` event on the composer instead of a `RedemptionRequested` event on the `AssetManager`.

Both scripts target the same composer contract (default `0xa10569DFb38FE7Be211aCe4E4A566Cea387023b0`). The rest of the flow — quoting the LayerZero fee, building the `SendParam`, checking the balance, sending the transaction, paying for the cross-chain message in ETH — is identical to the [Cross-Chain Redeem guide](/smart-accounts/guides/typescript-viem/cross-chain-redeem-ts).

## The compose message[​](#the-compose-message "Direct link to The compose message")

The destination composer expects a single ABI-encoded struct that mirrors the on-chain `IFAssetRedeemComposer.RedeemComposeMessage` type.

src/cross-chain-redeem-to-tag.ts

```
const redeemComposeMessageAbi = [  {    type: "tuple",    components: [      { name: "redeemer", type: "address" },      { name: "redeemerUnderlyingAddress", type: "string" },      { name: "redeemWithTag", type: "bool" },      { name: "destinationTag", type: "uint256" },      { name: "executor", type: "address" },      { name: "executorFee", type: "uint256" },    ],  },] as const;
```

The two `executor`\-related fields support paying a third party to advance the redemption on the user's behalf. In this script they are set to the zero address and zero amount, so the user themselves drives the flow.

src/cross-chain-redeem-to-tag.ts

```
function encodeComposeMessage(  redeemer: Address,  underlyingAddress: string,  destinationTag: bigint,): `0x${string}` {  return encodeAbiParameters(redeemComposeMessageAbi, [    {      redeemer,      redeemerUnderlyingAddress: underlyingAddress,      redeemWithTag: true,      destinationTag,      executor: zeroAddress,      executorFee: 0n,    },  ]);}
```

The `redeemWithTag` argument is hardcoded to `true` — that is the whole point of this guide. For tagless redemption against the same composer, see the [Cross-Chain Redeem guide](/smart-accounts/guides/typescript-viem/cross-chain-redeem-ts).

warning

Before the script can use `REDEMPTION_DESTINATION_TAG`, the tag has to be reserved on the [`IMintingTagManager`](/fassets/reference/IMintingTagManager) (via [`reserve()`](/fassets/reference/IMintingTagManager#reserve)) and bound to the redeemer with [`setMintingRecipient(tag, recipient)`](/fassets/reference/IMintingTagManager#setmintingrecipient). Without that registration the composer will refuse to process the redemption. See the [Redeem with Tag](/fassets/redemption#redeem-with-tag) section of the Redemption guide for the registration flow.

## Waiting for `FAssetRedeemed`[​](#waiting-for-fassetredeemed "Direct link to waiting-for-fassetredeemed")

Because the composer is the contract that pulls the trigger on the redemption, it emits its own event that fires after the underlying `AssetManager.redeem` call succeeds. The polling loop is the same shape as `waitForRedemptionOnCoston2` in the tagless redeem (25-block chunks, 5-minute timeout) but reads from the composer's ABI and filters on the user as the `redeemer`.

src/cross-chain-redeem-to-tag.ts

```
async function waitForFAssetRedeemedOnCoston2(  redeemer: Address,  fromBlock: bigint,) {  const deadline = Date.now() + REDEMPTION_TIMEOUT_MS;  let cursor = fromBlock;  while (Date.now() < deadline) {    const latest = await publicClient.getBlockNumber();    while (cursor <= latest) {      const chunkEnd = cursor + MAX_BLOCK_RANGE - 1n;      const toBlock = chunkEnd > latest ? latest : chunkEnd;      const logs = await publicClient.getContractEvents({        address: CONFIG.COSTON2_COMPOSER,        abi: coston2.ifAssetRedeemComposerAbi,        eventName: "FAssetRedeemed",        args: { redeemer },        fromBlock: cursor,        toBlock,      });      if (logs.length > 0) {        return logs[0]!;      }      cursor = toBlock + 1n;    }    await new Promise((resolve) =>      setTimeout(resolve, REDEMPTION_POLL_INTERVAL_MS),    );  }  throw new Error(    `FAssetRedeemed event not observed within ${REDEMPTION_TIMEOUT_MS}ms`,  );}
```

The `redeemer` argument here is the Sepolia signer address — i.e. the user — which makes the event easy to filter even if multiple unrelated redemptions are in flight against the same composer. All the other helpers used by this script (Sepolia client setup, `calculateAmountToSend`, the `SendParam` builder, the balance check, fee quote, and send path) are unchanged from the [Cross-Chain Redeem guide](/smart-accounts/guides/typescript-viem/cross-chain-redeem-ts).

## Full script[​](#full-script "Direct link to Full script")

The repository with the above example is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter).

src/cross-chain-redeem-to-tag.ts

```
import {  encodeAbiParameters,  formatUnits,  pad,  zeroAddress,  type Address,} from "viem";import { Options } from "@layerzerolabs/lz-v2-utilities";import { EndpointId } from "@layerzerolabs/lz-definitions";import { coston2 } from "@flarenetwork/flare-wagmi-periphery-package";import {  account,  sepoliaPublicClient,  sepoliaWalletClient,  publicClient,} from "./utils/client";import { abi as fxrpOftAbi } from "./abis/FXRPOFT";import { calculateAmountToSend } from "./utils/fassets";import type { SendParam } from "./types";const CONFIG = {  SEPOLIA_FXRP_OFT: process.env.SEPOLIA_FXRP_OFT as Address | undefined,  COSTON2_COMPOSER: (process.env.COSTON2_COMPOSER ??    "0xa10569DFb38FE7Be211aCe4E4A566Cea387023b0") as Address,  COSTON2_EID: EndpointId.FLARE_V2_TESTNET,  EXECUTOR_GAS: 1_000_000,  COMPOSE_GAS: 1_000_000,  SEND_LOTS: process.env.SEND_LOTS ?? "1",  XRP_ADDRESS: process.env.XRP_ADDRESS ?? "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp",  // XRPL destination tag pre-registered on the MintingTagManager and bound to a recipient.  // The tag must first be reserved (via `MintingTagManager.reserve()`) and tied to the  // redeemer using `setMintingRecipient(tag, recipient)` before this script can use it.  // See the "Redeem with Tag" section of the Redemption guide for the registration flow:  //   https://dev.flare.network/fassets/redemption#redeem-with-tag  REDEMPTION_DESTINATION_TAG: BigInt(    process.env.REDEMPTION_DESTINATION_TAG ?? "72",  ),} as const;const REDEMPTION_TIMEOUT_MS = 5 * 60 * 1000;const REDEMPTION_POLL_INTERVAL_MS = 10_000;// Coston2 RPC caps eth_getLogs to a 30-block range; chunk lookups to stay under it.const MAX_BLOCK_RANGE = 25n;// Mirrors `IFAssetRedeemComposer.RedeemComposeMessage`.const redeemComposeMessageAbi = [  {    type: "tuple",    components: [      { name: "redeemer", type: "address" },      { name: "redeemerUnderlyingAddress", type: "string" },      { name: "redeemWithTag", type: "bool" },      { name: "destinationTag", type: "uint256" },      { name: "executor", type: "address" },      { name: "executorFee", type: "uint256" },    ],  },] as const;function encodeComposeMessage(  redeemer: Address,  underlyingAddress: string,  destinationTag: bigint,): `0x${string}` {  return encodeAbiParameters(redeemComposeMessageAbi, [    {      redeemer,      redeemerUnderlyingAddress: underlyingAddress,      redeemWithTag: true,      destinationTag,      executor: zeroAddress,      executorFee: 0n,    },  ]);}function buildComposeOptions(): `0x${string}` {  return Options.newOptions()    .addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0)    .addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0)    .toHex() as `0x${string}`;}function buildSendParam(  amountToSend: bigint,  composeMsg: `0x${string}`,  extraOptions: `0x${string}`,): SendParam {  return {    dstEid: CONFIG.COSTON2_EID,    to: pad(CONFIG.COSTON2_COMPOSER, { size: 32 }),    amountLD: amountToSend,    minAmountLD: amountToSend,    extraOptions,    composeMsg,    oftCmd: "0x",  };}async function checkBalance(  oftAddress: Address,  signerAddress: Address,  amountToSend: bigint,  decimals: number,) {  const balance = await sepoliaPublicClient.readContract({    address: oftAddress,    abi: fxrpOftAbi,    functionName: "balanceOf",    args: [signerAddress],  });  console.log("\nCurrent FXRP balance:", formatUnits(balance, decimals));  if (balance < amountToSend) {    console.error("\nInsufficient FXRP balance!");    console.log("   Required:", formatUnits(amountToSend, decimals), "FXRP");    console.log("   Available:", formatUnits(balance, decimals), "FXRP");    throw new Error("Insufficient FXRP balance");  }}async function quoteFee(oftAddress: Address, sendParam: SendParam) {  const { nativeFee, lzTokenFee } = await sepoliaPublicClient.readContract({    address: oftAddress,    abi: fxrpOftAbi,    functionName: "quoteSend",    args: [sendParam, false],  });  console.log("\nLayerZero Fee:", formatUnits(nativeFee, 18), "ETH");  return { nativeFee, lzTokenFee };}async function executeSendAndRedeem(  oftAddress: Address,  sendParam: SendParam,  nativeFee: bigint,  lzTokenFee: bigint,  signerAddress: Address,  underlyingAddress: string,  destinationTag: bigint,) {  const { request } = await sepoliaPublicClient.simulateContract({    account,    address: oftAddress,    abi: fxrpOftAbi,    functionName: "send",    args: [sendParam, { nativeFee, lzTokenFee }, signerAddress],    value: nativeFee,  });  const txHash = await sepoliaWalletClient.writeContract(request);  const receipt = await sepoliaPublicClient.waitForTransactionReceipt({    hash: txHash,  });  console.log("\nTransaction sent:", txHash);  console.log("Confirmed in block:", receipt.blockNumber);  console.log("\nTrack your cross-chain transaction:");  console.log(`https://testnet.layerzeroscan.com/tx/${txHash}`);  console.log(    "\nThe auto-redeem-with-tag will execute once the message arrives on Coston2.",  );  console.log("XRP will be sent to:", underlyingAddress);  console.log("Destination tag:", destinationTag.toString());  return txHash;}async function waitForFAssetRedeemedOnCoston2(  redeemer: Address,  fromBlock: bigint,) {  const deadline = Date.now() + REDEMPTION_TIMEOUT_MS;  console.log("\nWaiting for FAssetRedeemed event on Coston2 composer...");  let cursor = fromBlock;  while (Date.now() < deadline) {    const latest = await publicClient.getBlockNumber();    while (cursor <= latest) {      const chunkEnd = cursor + MAX_BLOCK_RANGE - 1n;      const toBlock = chunkEnd > latest ? latest : chunkEnd;      const logs = await publicClient.getContractEvents({        address: CONFIG.COSTON2_COMPOSER,        abi: coston2.ifAssetRedeemComposerAbi,        eventName: "FAssetRedeemed",        args: { redeemer },        fromBlock: cursor,        toBlock,      });      if (logs.length > 0) {        return logs[0]!;      }      cursor = toBlock + 1n;    }    await new Promise((resolve) =>      setTimeout(resolve, REDEMPTION_POLL_INTERVAL_MS),    );  }  throw new Error(    `FAssetRedeemed event not observed within ${REDEMPTION_TIMEOUT_MS}ms`,  );}async function main() {  if (!CONFIG.SEPOLIA_FXRP_OFT) {    throw new Error(      "SEPOLIA_FXRP_OFT env var is required (address of the FXRP OFT on Sepolia)",    );  }  const oftAddress = CONFIG.SEPOLIA_FXRP_OFT;  const signerAddress = account.address;  console.log("Using account:", signerAddress);  console.log("Composer configured:", CONFIG.COSTON2_COMPOSER);  console.log("Connecting to FXRP OFT on Sepolia:", oftAddress);  const [decimals, amountToSend, startBlock] = await Promise.all([    sepoliaPublicClient.readContract({      address: oftAddress,      abi: fxrpOftAbi,      functionName: "decimals",    }),    calculateAmountToSend(BigInt(CONFIG.SEND_LOTS)),    publicClient.getBlockNumber(),  ]);  console.log("Token decimals:", decimals);  console.log("\nRedemption Parameters:");  console.log("Amount:", amountToSend.toString(), "FXRP base units");  console.log("XRP Address:", CONFIG.XRP_ADDRESS);  console.log("Redeemer:", signerAddress);  console.log("Destination tag:", CONFIG.REDEMPTION_DESTINATION_TAG.toString());  const composeMsg = encodeComposeMessage(    signerAddress,    CONFIG.XRP_ADDRESS,    CONFIG.REDEMPTION_DESTINATION_TAG,  );  const extraOptions = buildComposeOptions();  const sendParam = buildSendParam(amountToSend, composeMsg, extraOptions);  await checkBalance(oftAddress, signerAddress, amountToSend, decimals);  const { nativeFee, lzTokenFee } = await quoteFee(oftAddress, sendParam);  console.log(    "\nSending",    formatUnits(amountToSend, decimals),    "FXRP to Coston2 with auto-redeem-with-tag...",  );  console.log("Target composer:", CONFIG.COSTON2_COMPOSER);  await executeSendAndRedeem(    oftAddress,    sendParam,    nativeFee,    lzTokenFee,    signerAddress,    CONFIG.XRP_ADDRESS,    CONFIG.REDEMPTION_DESTINATION_TAG,  );  const redemptionEvent = await waitForFAssetRedeemedOnCoston2(    signerAddress,    startBlock,  );  console.log("\nFAssetRedeemed event observed on Coston2:");  console.log(redemptionEvent);}void main()  .then(() => process.exit(0))  .catch((error) => {    console.error(error);    process.exit(1);  });
```

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

```
Using account: 0xF5488132432118596fa13800B68df4C0fF25131dComposer configured: 0xa10569DFb38FE7Be211aCe4E4A566Cea387023b0Connecting to FXRP OFT on Sepolia: 0x81672c5d42F3573aD95A0bdfBE824FaaC547d4E6Token decimals: 6Redemption Parameters:Amount: 10000000 FXRP base unitsXRP Address: rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmpRedeemer: 0xF5488132432118596fa13800B68df4C0fF25131dDestination tag: 72Current FXRP balance: 70LayerZero Fee: 0.000101716112596574 ETHSending 10 FXRP to Coston2 with auto-redeem-with-tag...Target composer: 0xa10569DFb38FE7Be211aCe4E4A566Cea387023b0Transaction sent: 0xca0e2558b4397beef13c8b7b0acf6e9ecf42b22ea93aeec7e837656d7f23674cConfirmed in block: 10838169nTrack your cross-chain transaction:https://testnet.layerzeroscan.com/tx/0xca0e2558b4397beef13c8b7b0acf6e9ecf42b22ea93aeec7e837656d7f23674cThe auto-redeem-with-tag will execute once the message arrives on Coston2.XRP will be sent to: rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmpDestination tag: 72Waiting for FAssetRedeemed event on Coston2 composer...FAssetRedeemed event observed on Coston2:{  eventName: 'FAssetRedeemed',  args: {    guid: '0xaebbeadcb6b78b8ab0c541cefec4a5b9d037d4f069a7f333fb8b16f9b6439cb3',    srcEid: 40161,    redeemer: '0xF5488132432118596fa13800B68df4C0fF25131d',    redeemerAccount: '0xEB947e97BD6b02C4121D2AdBAa1e07A4cA3D8D1E',    amountToRedeemUBA: 9990000n,    redeemerUnderlyingAddress: 'rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp',    redeemWithTag: true,    destinationTag: 72n,    executor: '0x103b384064ae85577127097A7cCadfd6fb13f437',    executorFee: 0n,    redeemedAmountUBA: 9990000n,    wrappedAmount: 0n  },  address: '0xa10569dfb38fe7be211ace4e4a566cea387023b0',  topics: [    '0x9e8ed1306ec6ca165a2c23ae451b161741d3da7ab41fe134775ebd94b2351f04',    '0xaebbeadcb6b78b8ab0c541cefec4a5b9d037d4f069a7f333fb8b16f9b6439cb3',    '0x0000000000000000000000000000000000000000000000000000000000009ce1',    '0x000000000000000000000000f5488132432118596fa13800b68df4c0ff25131d'  ],  data: '0x000000000000000000000000eb947e97bd6b02c4121d2adbaa1e07a4ca3d8d1e0000000000000000000000000000000000000000000000000000000000986f70000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000048000000000000000000000000103b384064ae85577127097a7ccadfd6fb13f43700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000986f7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022727048757734624b536a6f6e4b52724b4b5659555a595956656467316a7950726d70000000000000000000000000000000000000000000000000000000000000',  blockNumber: 30438770n,  transactionHash: '0xc1d39bc7383e856a92f9a3a60f1b33f89926a21c68910f29a3566c1a0f557679',  transactionIndex: 9,  blockHash: '0x2071fdbb5d07266f380a40c819818fff507f7142340961681af5a8ec796e6b09',  logIndex: 18,  removed: false,  blockTimestamp: undefined}
```

## What's next[​](#whats-next "Direct link to What's next")

-   [Cross-Chain Mint guide](/smart-accounts/guides/typescript-viem/cross-chain-mint-ts)
-   [Cross-Chain Redeem guide](/smart-accounts/guides/typescript-viem/cross-chain-redeem-ts)
-   [Direct Minting overview](/fassets/direct-minting)
