FAsset Auto-Redemption
Overview
In this guide, you will learn how to bridge FAssets (specifically FXRP) between Flare Testnet Coston2 and Hyperliquid using LayerZero's cross-chain messaging protocol, with support for automatic redemption - converting FXRP back to native XRP on the XRP Ledger in a single transaction.
This guide covers four key functionalities:
- Bridging FXRP from Flare Testnet Coston2 to Hyperliquid EVM (
bridgeToHyperEVM.ts) - Transfer wrapped XRP tokens to Hyperliquid EVM for DeFi use. - Bridging FXRP from Flare Testnet Coston2 to HyperCore (
bridgeToHyperCore.ts) - Transfer wrapped XRP tokens directly to HyperCore for spot trading. - Auto-Redeem from Hyperliquid EVM to Underlying Asset (
autoRedeemFromHyperEVM.ts) - Send FXRP from Hyperliquid EVM back to Flare Testnet Coston2 and automatically redeem it for native XRP. - Auto-Redeem from HyperCore to Underlying Asset (
autoRedeemFromHyperCore.ts) - Transfer FXRP from HyperCore spot wallet to HyperEVM, then automatically bridge and redeem to native XRP in one script.
Key technologies:
- LayerZero OFT (Omnichain Fungible Token) for cross-chain token transfers. OFT works by burning tokens on the source chain and minting equivalent tokens on the destination chain, enabling seamless movement of assets across different blockchains.
- Flare's FAsset system for tokenizing non-smart contract assets.
- LayerZero Composer pattern for executing custom logic (like FAsset redemption) automatically when tokens arrive on the destination chain.
- Hyperliquid spotSend API for transferring tokens between HyperCore and HyperEVM.
This guide includes a Solidity smart contract (FAssetRedeemComposer.sol) and four TypeScript scripts (bridgeToHyperEVM.ts, bridgeToHyperCore.ts, autoRedeemFromHyperEVM.ts, and autoRedeemFromHyperCore.ts) that demonstrate the complete workflows.
Clone the Flare Hardhat Starter to follow along.
Getting Started: The Two-Step Process
Step 1: Bridge FXRP to Hyperliquid EVM (Required First)
Run bridgeToHyperEVM.ts on Flare Testnet Coston2 network to transfer your FXRP tokens from Coston2 to Hyperliquid EVM Testnet.
This script does NOT involve auto-redemption - it simply moves your tokens to Hyperliquid where you can use them to prepare for the auto-redemption process.
You need FXRP tokens on Hyperliquid EVM before you can test the auto-redeem functionality. The auto-redeem script will fail if you don't have tokens there.
# Step 1: Bridge tokens TO Hyperliquid
yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2
Step 2: Auto-Redeem to Native XRP (Requires Step 1)
Once you have FXRP tokens on Hyperliquid (from Step 1), run autoRedeemFromHyperEVM.ts on Hyperliquid Testnet network to send them back to Flare Testnet Coston2 with automatic redemption to native XRP.
This is the auto-redemption feature that converts FXRP back to native XRP on the XRP Ledger in a single transaction.
# Step 2: Auto-redeem back to native XRP
yarn hardhat run scripts/fassets/autoRedeemFromHyperEVM.ts --network hyperliquidTestnet
FAssetRedeemComposer Contract
What It Is
FAssetRedeemComposer.sol is a LayerZero Composer contract that automatically redeems FAssets to their underlying assets when tokens arrive on Flare Testnet Coston2 via LayerZero's compose message feature.
How It Works
The composer implements the IOAppComposer interface and processes LayerZero compose messages:
- Receives Compose Message: LayerZero endpoint calls lzCompose() with the incoming OFT transfer and compose data.
- Extracts Parameters: Decodes the compose message to get:
- Amount to redeem (in lots).
- Underlying XRP address on the XRP Ledger.
- Redeemer EVM address on Flare Chain.
- Gets Asset Manager: Retrieves the FAsset AssetManager from Flare's ContractRegistry.
- Calculates Lots: Determines how many lots can be redeemed based on received balance.
- Approves Tokens: Grants AssetManager permission to spend the FAsset tokens (which is in the ERC-20 standard).
- Executes Redemption: Calls
assetManager.redeem()to burn FAssets and release underlying XRP.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {IOAppComposer} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppComposer.sol";
import {OFTComposeMsgCodec} from "@layerzerolabs/oft-evm/contracts/libs/OFTComposeMsgCodec.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IAssetManager} from "@flarenetwork/flare-periphery-contracts/coston2/IAssetManager.sol";
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
contract FAssetRedeemComposer is IOAppComposer, Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
address public immutable endpoint;
event RedemptionTriggered(
address indexed redeemer,
string underlyingAddress,
uint256 indexed amountRedeemed,
uint256 indexed lots
);
error OnlyEndpoint();
error InsufficientBalance();
error AmountTooSmall();
constructor(address _endpoint) Ownable(msg.sender) {
endpoint = _endpoint;
}
receive() external payable {}
function lzCompose(
address /* _from */,
bytes32 /* _guid */,
bytes calldata _message,
address /* _executor */,
bytes calldata /* _extraData */
) external payable override nonReentrant {
if (msg.sender != endpoint) revert OnlyEndpoint();
bytes memory composeMsg = OFTComposeMsgCodec.composeMsg(_message);
_processRedemption(composeMsg);
}
function _processRedemption(bytes memory composeMsg) internal {
// 1. Decode message
(, string memory underlyingAddress, address redeemer) = abi.decode(
composeMsg,
(uint256, string, address)
);
// 2. Get Asset Manager & fXRP Token from Registry
IAssetManager assetManager = ContractRegistry.getAssetManagerFXRP();
IERC20 fAssetToken = IERC20(address(assetManager.fAsset()));
// 3. Check Actual Balance received from LayerZero
uint256 currentBalance = fAssetToken.balanceOf(address(this));
if (currentBalance == 0) revert InsufficientBalance();
// 4. Calculate Lots
uint256 lotSizeUBA = assetManager.getSettings().lotSizeAMG;
uint256 lots = currentBalance / lotSizeUBA;
if (lots == 0) revert AmountTooSmall();
// 5. Calculate amount to burn
uint256 amountToRedeem = lots * lotSizeUBA;
// 6. Approve AssetManager to spend the tokens
fAssetToken.forceApprove(address(assetManager), amountToRedeem);
// 7. Redeem
uint256 redeemedAmount = assetManager.redeem(
lots,
underlyingAddress,
payable(address(0))
);
emit RedemptionTriggered(
redeemer,
underlyingAddress,
redeemedAmount,
lots
);
}
function recoverTokens(
address token,
address to,
uint256 amount
) external onlyOwner {
IERC20(token).safeTransfer(to, amount);
}
function recoverNative() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
Code Breakdown
The _processRedemption function contains the core redemption logic with numbered steps in the comments:
- Decode Message: Extracts the underlying XRP address and redeemer EVM address from the compose message.
- Get Asset Manager & fXRP Token: Retrieves the FAsset AssetManager.
- Check Balance: Verifies that tokens were received from LayerZero. Reverts with
InsufficientBalance()if balance is zero. - Calculate Lots: Determines how many complete lots can be redeemed based on the received balance and the lot size from AssetManager settings.
- Calculate Amount: Computes the exact amount to redeem (must be a multiple of lot size). Reverts with
AmountTooSmall()if less than 1 lot. - Approve AssetManager: Grants the AssetManager permission to spend the FAsset tokens using the ERC-20
approvepattern. - Redeem: Calls
assetManager.redeem()to burn FAssets and trigger the release of underlying XRP to the specified address.
The contract also includes recoverTokens() and recoverNative() helper functions that allow the owner to recover any stuck tokens or native currency if needed.
Bridge FXRP to Hyperliquid EVM (Step 1)
What It Is
This is Step 1 of the two-step process and is a prerequisite for testing the auto-redemption feature.
This script bridges FXRP tokens from Flare Testnet Coston2 to Hyperliquid EVM Testnet using LayerZero's OFT Adapter pattern. It wraps existing ERC20 FAsset tokens into LayerZero OFT format for cross-chain transfer.
This script does NOT perform auto-redemption.
Its purpose is to get your FXRP tokens onto Hyperliquid EVM Testnet so that:
- You can use them for trading or DeFi on Hyperliquid, OR
- You can run the auto-redeem script (Step 2) to convert them back to native XRP
You must successfully complete this step before running the autoRedeemFromHyperEVM.ts script.
How It Works
Step-by-Step Process
- Get Asset Manager Info: Retrieves fAsset address and lot size dynamically from the AssetManager contract using the
getAssetManagerFXRPutility. - Balance Check: Verifies user has sufficient FTestXRP tokens.
- Token Approval:
- Approves OFT Adapter to spend FTestXRP.
- Approves Composer (if needed for future operations).
- Build Send Parameters:
- Destination: Hyperliquid EVM Testnet (EndpointID for Hyperliquid Testnet:
40294). - Recipient: Same address on destination chain.
- Amount: Calculated from configured lots (default 1 lot, with 10% buffer for safety).
- LayerZero options: Executor gas limit set to 200,000.
- Destination: Hyperliquid EVM Testnet (EndpointID for Hyperliquid Testnet:
- Quote Fee: Calculates the LayerZero cross-chain messaging fee.
- Execute Bridge: Sends tokens via
oftAdapter.send(). - Confirmation: Waits for transaction confirmation and provides tracking link to the LayerZero Explorer page.
Prerequisites
- Balance Requirements:
- FTestXRP tokens (amount you want to bridge).
- C2FLR tokens (for gas fees + LayerZero fees). You can get some from the Flare Testnet faucet.
- Environment Setup:
- Private key configured in Hardhat for Flare Testnet Coston2 and Hyperliquid Testnet.
Configuration
Edit the CONFIG object in the script:
const CONFIG = {
COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",
COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "",
HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET,
EXECUTOR_GAS: 200_000,
BRIDGE_LOTS: "1", // Change this to your desired number of lots
};
How to Run
Run this script FIRST before attempting auto-redemption. This gets your FXRP tokens onto Hyperliquid EVM.
-
Install Dependencies:
yarn install -
Configure Environment:
# .env file
COSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpc
DEPLOYER_PRIVATE_KEY=your_private_key_here
COSTON2_COMPOSER=0x... # Optional for this step, required for Step 2 -
Run the Script on Flare Testnet Coston2:
yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2 -
Wait for Completion:
- Monitor the transaction on LayerZero Scan (link provided in output)
- Allow 2-5 minutes for cross-chain delivery
- Verify FXRP balance increased on Hyperliquid EVM before proceeding to Step 2
Expected Output
Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
📋 Bridge Details:
From: Coston2
To: Hyperliquid EVM Testnet
Amount: 11.0 FXRP
Recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Your FTestXRP balance: 100.0
1️⃣ Checking OFT Adapter token address...
OFT Adapter's inner token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F
Expected token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F
Match: true
Approving FTestXRP for OFT Adapter...
OFT Adapter address: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639
Amount: 11.0 FXRP
✅ OFT Adapter approved
Verified allowance: 22.0 FXRP
3️⃣ LayerZero Fee: 0.001234 C2FLR
4️⃣ Sending FXRP to Hyperliquid EVM Testnet...
Transaction sent: 0xabc123...
✅ Confirmed in block: 12345678
🎉 Success! Your FXRP is on the way to Hyperliquid EVM Testnet!
Track your transaction:
https://testnet.layerzeroscan.com/tx/0xabc123...
It may take a few minutes to arrive on Hyperliquid EVM Testnet.
View bridgeToHyperEVM.ts source code
/**
* Bridge FXRP from Coston2 to Hyperliquid EVM Testnet
*
* This script helps you get FXRP on Hyperliquid EVM Testnet by bridging from Coston2
*
* Prerequisites:
* - FTestXRP tokens on Coston2
* - CFLR on Coston2 for gas
*
* Usage:
* yarn hardhat run scripts/fassets/bridgeToHyperEVM.ts --network coston2
*/
import { web3 } from "hardhat";
import { formatUnits } from "ethers";
import { EndpointId } from "@layerzerolabs/lz-definitions";
import { Options } from "@layerzerolabs/lz-v2-utilities";
import type {
IERC20MetadataInstance,
FAssetOFTAdapterInstance,
} from "../../typechain-types";
import { getAssetManagerFXRP } from "../utils/getters";
const IERC20Metadata = artifacts.require("IERC20Metadata");
const FAssetOFTAdapter = artifacts.require("FAssetOFTAdapter");
const CONFIG = {
COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",
COSTON2_COMPOSER: process.env.COSTON2_COMPOSER || "",
HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET,
EXECUTOR_GAS: 200_000,
BRIDGE_LOTS: "1",
} as const;
type BridgeParams = {
amountToBridge: bigint;
recipientAddress: string;
signerAddress: string;
fAssetAddress: string;
};
type SendParams = {
dstEid: EndpointId;
to: string;
amountLD: string;
minAmountLD: string;
extraOptions: string;
composeMsg: string;
oftCmd: string;
};
async function getAssetManagerInfo(lots: bigint) {
const assetManager = await getAssetManagerFXRP();
const fAssetAddress = await assetManager.fAsset();
const lotSizeBN = await assetManager.lotSize();
const lotSize = BigInt(lotSizeBN.toString());
const amountToBridge = lotSize * lots;
return {
fAssetAddress,
amountToBridge: (amountToBridge * 11n) / 10n, // 10% buffer
};
}
/**
* Gets the signer and displays account information
*/
async function getSigner(fAssetAddress: string) {
const accounts = await web3.eth.getAccounts();
const signerAddress = accounts[0];
console.log("Using account:", signerAddress);
console.log("Token address:", fAssetAddress);
return signerAddress;
}
/**
* Prepares bridge parameters
*/
function prepareBridgeParams(
signerAddress: string,
fAssetAddress: string,
amountToBridge: bigint,
decimals: number,
): BridgeParams {
const recipientAddress = signerAddress;
console.log("\n📋 Bridge Details:");
console.log("From: Coston2");
console.log("To: Hyperliquid EVM Testnet");
console.log(
"Amount:",
formatUnits(amountToBridge.toString(), decimals),
"FXRP",
);
console.log("Recipient:", recipientAddress);
return { amountToBridge, recipientAddress, signerAddress, fAssetAddress };
}
/**
* Checks if user has sufficient balance to bridge
*/
async function checkBalance(
params: BridgeParams,
decimals: number,
): Promise<IERC20MetadataInstance> {
const fAsset: IERC20MetadataInstance = await IERC20Metadata.at(
params.fAssetAddress,
);
const balance = await fAsset.balanceOf(params.signerAddress);
console.log(
"\nYour FTestXRP balance:",
formatUnits(balance.toString(), decimals),
);
if (BigInt(balance.toString()) < params.amountToBridge) {
console.error("\n❌ Insufficient FTestXRP balance!");
console.log(" Token address: " + params.fAssetAddress);
throw new Error("Insufficient balance");
}
return fAsset;
}
/**
* Approves OFT Adapter AND Composer to spend FTestXRP
*/
async function approveTokens(
fAsset: IERC20MetadataInstance,
amountToBridge: bigint,
signerAddress: string,
fAssetAddress: string,
decimals: number,
): Promise<FAssetOFTAdapterInstance> {
const oftAdapter: FAssetOFTAdapterInstance = await FAssetOFTAdapter.at(
CONFIG.COSTON2_OFT_ADAPTER,
);
console.log("\n1️⃣ Checking OFT Adapter token address...");
const underlyingToken = await oftAdapter.token();
console.log(" OFT Adapter's underlying token:", underlyingToken);
console.log(" Expected token:", fAssetAddress);
console.log(
" Match:",
underlyingToken.toLowerCase() === fAssetAddress.toLowerCase(),
);
console.log("\n Approving FTestXRP for OFT Adapter...");
console.log(" OFT Adapter address:", oftAdapter.address);
console.log(
" Amount:",
formatUnits(amountToBridge.toString(), decimals),
"FXRP",
);
const amount = amountToBridge;
await fAsset.approve(oftAdapter.address, amount.toString());
console.log("✅ OFT Adapter approved");
// Verify the allowance
const allowance1 = await fAsset.allowance(signerAddress, oftAdapter.address);
console.log(
" Verified allowance:",
formatUnits(allowance1.toString(), decimals),
"FXRP",
);
console.log("\n2️⃣ Approving FTestXRP for Composer...");
console.log(" Composer address:", CONFIG.COSTON2_COMPOSER);
await fAsset.approve(CONFIG.COSTON2_COMPOSER, amountToBridge.toString());
console.log("✅ Composer approved");
// Verify the allowance
const allowance2 = await fAsset.allowance(
signerAddress,
CONFIG.COSTON2_COMPOSER,
);
console.log(
" Verified allowance:",
formatUnits(allowance2.toString(), decimals),
"FXRP",
);
return oftAdapter;
}
/**
* Builds LayerZero send parameters
*/
function buildSendParams(params: BridgeParams): SendParams {
const options = Options.newOptions().addExecutorLzReceiveOption(
CONFIG.EXECUTOR_GAS,
0,
);
return {
dstEid: CONFIG.HYPERLIQUID_EID as EndpointId,
to: web3.utils.padLeft(params.recipientAddress, 64), // 32 bytes = 64 hex chars
amountLD: params.amountToBridge.toString(),
minAmountLD: params.amountToBridge.toString(),
extraOptions: options.toHex(),
composeMsg: "0x",
oftCmd: "0x",
};
}
/**
* Quotes the LayerZero fee for the bridge transaction
*/
async function quoteFee(
oftAdapter: FAssetOFTAdapterInstance,
sendParam: SendParams,
) {
const result = await oftAdapter.quoteSend(sendParam, false);
const nativeFee = BigInt(result.nativeFee.toString());
console.log(
"\n3️⃣ LayerZero Fee:",
formatUnits(nativeFee.toString(), 18),
"C2FLR",
);
return nativeFee;
}
/**
* Executes the bridge transaction
*/
async function executeBridge(
oftAdapter: FAssetOFTAdapterInstance,
sendParam: SendParams,
nativeFee: bigint,
signerAddress: string,
): Promise<void> {
console.log("\n4️⃣ Sending FXRP to Hyperliquid EVM Testnet...");
const tx = await oftAdapter.send(
sendParam,
{ nativeFee: nativeFee.toString(), lzTokenFee: "0" },
signerAddress,
{
value: nativeFee.toString(),
},
);
console.log("Transaction sent:", tx.tx);
console.log("✅ Confirmed in block:", tx.receipt.blockNumber);
console.log(
"\n🎉 Success! Your FXRP is on the way to Hyperliquid EVM Testnet!",
);
console.log("\nTrack your transaction:");
console.log(`https://testnet.layerzeroscan.com/tx/${tx.tx}`);
console.log(
"\nIt may take a few minutes to arrive on Hyperliquid EVM Testnet.",
);
}
async function main() {
// 1. Get fAsset address and amount from AssetManager
const { fAssetAddress, amountToBridge } = await getAssetManagerInfo(
BigInt(CONFIG.BRIDGE_LOTS),
);
// 2. Get signer and display account info
const signerAddress = await getSigner(fAssetAddress);
// 3. Get token decimals
const fAssetToken: IERC20MetadataInstance =
await IERC20Metadata.at(fAssetAddress);
const decimals = Number(await fAssetToken.decimals());
console.log("Token decimals:", decimals);
// 4. Prepare bridge parameters
const params = prepareBridgeParams(
signerAddress,
fAssetAddress,
amountToBridge,
decimals,
);
// 5. Check balance and get token contract
const fAsset = await checkBalance(params, decimals);
// 6. Approve tokens and get OFT adapter
const oftAdapter = await approveTokens(
fAsset,
params.amountToBridge,
signerAddress,
fAssetAddress,
decimals,
);
// 7. Build send parameters
const sendParam = buildSendParams(params);
// 8. Quote the fee
const nativeFee = await quoteFee(oftAdapter, sendParam);
// 9. Execute the bridge transaction
await executeBridge(oftAdapter, sendParam, nativeFee, signerAddress);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Troubleshooting
Error: Insufficient FTestXRP balance
- Solution: Acquire FTestXRP tokens on Flare Testnet Coston2 faucet or follow our Fasset minting guide.
Error: Insufficient C2FLR for gas
- Solution: Get C2FLR from Flare Testnet Coston2 faucet.
Error: Transaction reverted
- Check that the OFT Adapter address matches the FTestXRP token.
- Verify LayerZero endpoint is operational.
- Ensure gas limits are sufficient.
Bridge FXRP to HyperCore
What It Is
This script bridges FXRP tokens from Flare Testnet Coston2 directly to Hyperliquid HyperCore (the spot trading layer) using LayerZero's OFT Adapter with a compose message.
Unlike bridgeToHyperEVM.ts which deposits tokens on HyperEVM, this script triggers automatic transfer to HyperCore where you can trade FXRP on Hyperliquid's spot market.
- bridgeToHyperEVM.ts: Deposits FXRP on HyperEVM (EVM layer) for DeFi use.
- bridgeToHyperCore.ts: Deposits FXRP on HyperCore (trading layer) for spot trading.
Choose based on whether you want to use FXRP in smart contracts (HyperEVM) or trade it (HyperCore).
How It Works
Flow Diagram
┌─────────────────────────┐
│ Developer │
│ (Coston2) │
└────────┬────────────────┘
│
│ 1. Has FXRP tokens
▼
┌─────────────────────────┐
│ FXRP (Coston2) │
│ Balance: 11 FXRP │
└────────┬────────────────┘
│
│ 2. Send via OFT Adapter
│ with Compose Message
▼
┌─────────────────────────┐
│ LayerZero Endpoint │
│ - lzSend() │
│ - Compose enabled │
└────────┬────────────────┘
│
│ 3. Cross-chain message
▼
┌─────────────────────────┐
│ HyperEVM Endpoint │
│ - lzReceive() │
│ - lzCompose() │
└────────┬────────────────┘
│
│ 4. Calls Composer
▼
┌─────────────────────────┐
│ HyperliquidComposer │
│ - Receives FXRP │
│ - Transfers to system │
│ address │
└────────┬────────────────┘
│
│ 5. HyperCore credit
▼
┌─────────────────────────┐
│ HyperCore Spot Wallet │
│ FXRP credited │
└─────────────────────────┘
Step-by-Step Process
- Validate Config: Checks that
HYPERLIQUID_COMPOSERis configured. - Get Asset Manager Info: Retrieves fAsset address and lot size dynamically from the AssetManager contract.
- Balance Check: Verifies user has sufficient FTestXRP tokens.
- Token Approval: Approves OFT Adapter to spend FTestXRP.
- Encode Compose Message: Encodes
(uint256 amount, address recipient)for HyperCore transfer. - Build LayerZero Options:
- Executor gas for
lzReceive(): 200,000 - Compose gas for
lzCompose(): 300,000
- Executor gas for
- Build Send Parameters:
- Destination: HyperEVM (EndpointID for Hyperliquid Testnet:
40294). - Recipient: HyperliquidComposer contract (not user address).
- Amount: Calculated from configured lots (default 1 lot, with 10% buffer).
- Destination: HyperEVM (EndpointID for Hyperliquid Testnet:
- Quote Fee: Calculates the LayerZero cross-chain messaging fee.
- Execute Bridge: Sends tokens via
oftAdapter.send()with compose. - HyperCore Credit: Upon arrival, composer transfers tokens to HyperCore.
Prerequisites
- Balance Requirements:
- FTestXRP tokens on Coston2 (amount you want to bridge).
- C2FLR tokens (for gas fees + LayerZero fees). You can get some from the Flare Testnet faucet.
- Deployed Contracts:
- HyperliquidComposer must be deployed on HyperEVM Testnet.
- Composer address must be set in
.env.
Configuration
Edit the CONFIG object in the script:
const CONFIG = {
COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",
HYPERLIQUID_COMPOSER: process.env.HYPERLIQUID_COMPOSER || "",
HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET,
EXECUTOR_GAS: 200_000, // Gas for receiving on HyperEVM
COMPOSE_GAS: 300_000, // Gas for compose execution
BRIDGE_LOTS: "1", // Change this to your desired number of lots
};
How to Run
-
Deploy HyperliquidComposer (first time only):
yarn hardhat run scripts/fassets/deployHyperliquidComposer.ts --network hyperliquidTestnet -
Configure Environment:
# .env file
COSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpc
DEPLOYER_PRIVATE_KEY=your_private_key_here
HYPERLIQUID_COMPOSER=0x... # Required! Set this after deploying the composer -
Run the Script on Flare Testnet Coston2:
yarn hardhat run scripts/fassets/bridgeToHyperCore.ts --network coston2 -
Wait for Completion:
- Monitor the transaction on LayerZero Scan (link provided in output).
- Allow 2-5 minutes for cross-chain delivery.
- Verify FXRP balance on HyperCore via Hyperliquid's UI or API.
Expected Output
✓ HyperliquidComposer configured: 0x123...
Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Token address: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F
Token decimals: 6
📋 Bridge Details:
From: Coston2
To: Hyperliquid HyperCore (via HyperEVM)
Amount: 11.0 FXRP
HyperCore Recipient: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Your FTestXRP balance: 100.0
1️⃣ Checking OFT Adapter token address...
OFT Adapter's underlying token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F
Expected token: 0x8b4abA9C4BD7DD961659b02129beE20c6286e17F
Match: true
Approving FTestXRP for OFT Adapter...
OFT Adapter address: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639
Amount: 11.0 FXRP
✅ OFT Adapter approved
Verified allowance: 11.0 FXRP
2️⃣ Compose message encoded for HyperCore transfer
3️⃣ LayerZero Fee: 0.001234 C2FLR
4️⃣ Sending FXRP to Hyperliquid HyperCore...
Via HyperliquidComposer: 0x123...
Transaction sent: 0xabc123...
✅ Confirmed in block: 12345678
🎉 Success! Your FXRP is on the way to Hyperliquid HyperCore!
Track your transaction:
https://testnet.layerzeroscan.com/tx/0xabc123...
⏳ The tokens will be automatically transferred to HyperCore once they arrive on HyperEVM.
You can then trade them on the Hyperliquid DEX.
View bridgeToHyperCore.ts source code
/**
* Bridge FXRP from Coston2 to Hyperliquid HyperCore
*
* This script bridges FXRP from Coston2 to HyperEVM with a compose message
* that triggers automatic transfer to HyperCore (the trading layer).
*
* Flow:
* 1. Send FXRP via LayerZero from Coston2 to HyperEVM
* 2. LayerZero delivers to HyperliquidComposer on HyperEVM
* 3. Composer transfers tokens to system address, crediting them on HyperCore
*
* Prerequisites:
* - FTestXRP tokens on Coston2
* - CFLR on Coston2 for gas
* - HyperliquidComposer deployed on HyperEVM
*
* Usage:
* yarn hardhat run scripts/fassets/bridgeToHyperCore.ts --network coston2
*/
import { web3 } from "hardhat";
import { formatUnits } from "ethers";
import { EndpointId } from "@layerzerolabs/lz-definitions";
import { Options } from "@layerzerolabs/lz-v2-utilities";
import type {
IERC20MetadataInstance,
FAssetOFTAdapterInstance,
} from "../../typechain-types";
import { getAssetManagerFXRP } from "../utils/getters";
const IERC20Metadata = artifacts.require("IERC20Metadata");
const FAssetOFTAdapter = artifacts.require("FAssetOFTAdapter");
const CONFIG = {
COSTON2_OFT_ADAPTER: "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639",
HYPERLIQUID_COMPOSER: process.env.HYPERLIQUID_COMPOSER || "",
HYPERLIQUID_EID: EndpointId.HYPERLIQUID_V2_TESTNET,
EXECUTOR_GAS: 200_000,
COMPOSE_GAS: 300_000,
BRIDGE_LOTS: "1",
} as const;
type BridgeParams = {
amountToBridge: bigint;
recipientAddress: string;
signerAddress: string;
fAssetAddress: string;
};
type SendParams = {
dstEid: EndpointId;
to: string;
amountLD: string;
minAmountLD: string;
extraOptions: string;
composeMsg: string;
oftCmd: string;
};
/**
* Validates that required configuration is set
*/
function validateConfig() {
if (!CONFIG.HYPERLIQUID_COMPOSER) {
throw new Error(
"HYPERLIQUID_COMPOSER not set in .env!\n" +
" Deploy HyperliquidComposer first on HyperEVM:\n" +
" yarn hardhat run scripts/fassets/deployHyperliquidComposer.ts --network hyperliquidTestnet",
);
}
console.log("✓ HyperliquidComposer configured:", CONFIG.HYPERLIQUID_COMPOSER);
}
/**
* Gets fAsset address and calculates amount from AssetManager
*/
async function getAssetManagerInfo(lots: bigint) {
const assetManager = await getAssetManagerFXRP();
const fAssetAddress = await assetManager.fAsset();
const lotSizeBN = await assetManager.lotSize();
const lotSize = BigInt(lotSizeBN.toString());
const amountToBridge = lotSize * lots;
return {
fAssetAddress,
amountToBridge: (amountToBridge * 11n) / 10n, // 10% buffer
};
}
/**
* Gets the signer and displays account information
*/
async function getSigner(fAssetAddress: string) {
const accounts = await web3.eth.getAccounts();
const signerAddress = accounts[0];
console.log("Using account:", signerAddress);
console.log("Token address:", fAssetAddress);
return signerAddress;
}
/**
* Prepares bridge parameters
*/
function prepareBridgeParams(
signerAddress: string,
fAssetAddress: string,
amountToBridge: bigint,
decimals: number,
): BridgeParams {
const recipientAddress = signerAddress;
console.log("\n📋 Bridge Details:");
console.log("From: Coston2");
console.log("To: Hyperliquid HyperCore (via HyperEVM)");
console.log(
"Amount:",
formatUnits(amountToBridge.toString(), decimals),
"FXRP",
);
console.log("HyperCore Recipient:", recipientAddress);
return { amountToBridge, recipientAddress, signerAddress, fAssetAddress };
}
/**
* Checks if user has sufficient balance to bridge
*/
async function checkBalance(
params: BridgeParams,
decimals: number,
): Promise<IERC20MetadataInstance> {
const fAsset: IERC20MetadataInstance = await IERC20Metadata.at(
params.fAssetAddress,
);
const balance = await fAsset.balanceOf(params.signerAddress);
console.log(
"\nYour FTestXRP balance:",
formatUnits(balance.toString(), decimals),
);
if (BigInt(balance.toString()) < params.amountToBridge) {
console.error("\n❌ Insufficient FTestXRP balance!");
console.log(" Token address: " + params.fAssetAddress);
throw new Error("Insufficient balance");
}
return fAsset;
}
/**
* Approves OFT Adapter to spend FTestXRP
*/
async function approveTokens(
fAsset: IERC20MetadataInstance,
amountToBridge: bigint,
signerAddress: string,
fAssetAddress: string,
decimals: number,
): Promise<FAssetOFTAdapterInstance> {
const oftAdapter: FAssetOFTAdapterInstance = await FAssetOFTAdapter.at(
CONFIG.COSTON2_OFT_ADAPTER,
);
console.log("\n1️⃣ Checking OFT Adapter token address...");
const underlyingToken = await oftAdapter.token();
console.log(" OFT Adapter's underlying token:", underlyingToken);
console.log(" Expected token:", fAssetAddress);
console.log(
" Match:",
underlyingToken.toLowerCase() === fAssetAddress.toLowerCase(),
);
console.log("\n Approving FTestXRP for OFT Adapter...");
console.log(" OFT Adapter address:", oftAdapter.address);
console.log(
" Amount:",
formatUnits(amountToBridge.toString(), decimals),
"FXRP",
);
await fAsset.approve(oftAdapter.address, amountToBridge.toString());
console.log("✅ OFT Adapter approved");
// Verify the allowance
const allowance = await fAsset.allowance(signerAddress, oftAdapter.address);
console.log(
" Verified allowance:",
formatUnits(allowance.toString(), decimals),
"FXRP",
);
return oftAdapter;
}
/**
* Encodes the compose message for HyperCore transfer
* Format: (amount, recipientAddress)
*/
function encodeComposeMessage(params: BridgeParams): string {
// Encode: (amount, recipient) - recipient will receive tokens on HyperCore
const composeMsg = web3.eth.abi.encodeParameters(
["uint256", "address"],
[params.amountToBridge.toString(), params.recipientAddress],
);
console.log("\n2️⃣ Compose message encoded for HyperCore transfer");
return composeMsg;
}
/**
* Builds LayerZero options with compose support
*/
function buildComposeOptions(): string {
const options = Options.newOptions()
.addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0)
.addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0);
return options.toHex();
}
/**
* Builds LayerZero send parameters with compose message
*/
function buildSendParams(
params: BridgeParams,
composeMsg: string,
options: string,
): SendParams {
return {
dstEid: CONFIG.HYPERLIQUID_EID as EndpointId,
to: web3.utils.padLeft(CONFIG.HYPERLIQUID_COMPOSER, 64), // Send to composer, not recipient
amountLD: params.amountToBridge.toString(),
minAmountLD: params.amountToBridge.toString(),
extraOptions: options,
composeMsg: composeMsg,
oftCmd: "0x",
};
}
/**
* Quotes the LayerZero fee for the bridge transaction
*/
async function quoteFee(
oftAdapter: FAssetOFTAdapterInstance,
sendParam: SendParams,
) {
const result = await oftAdapter.quoteSend(sendParam, false);
const nativeFee = BigInt(result.nativeFee.toString());
console.log(
"\n3️⃣ LayerZero Fee:",
formatUnits(nativeFee.toString(), 18),
"C2FLR",
);
return nativeFee;
}
/**
* Executes the bridge transaction with compose
*/
async function executeBridge(
oftAdapter: FAssetOFTAdapterInstance,
sendParam: SendParams,
nativeFee: bigint,
signerAddress: string,
): Promise<void> {
console.log("\n4️⃣ Sending FXRP to Hyperliquid HyperCore...");
console.log(" Via HyperliquidComposer:", CONFIG.HYPERLIQUID_COMPOSER);
const tx = await oftAdapter.send(
sendParam,
{ nativeFee: nativeFee.toString(), lzTokenFee: "0" },
signerAddress,
{
value: nativeFee.toString(),
},
);
console.log("Transaction sent:", tx.tx);
console.log("✅ Confirmed in block:", tx.receipt.blockNumber);
console.log(
"\n🎉 Success! Your FXRP is on the way to Hyperliquid HyperCore!",
);
console.log("\nTrack your transaction:");
console.log(`https://testnet.layerzeroscan.com/tx/${tx.tx}`);
console.log(
"\n⏳ The tokens will be automatically transferred to HyperCore once they arrive on HyperEVM.",
);
console.log("You can then trade them on the Hyperliquid DEX.");
}
async function main() {
// 0. Validate configuration
validateConfig();
// 1. Get fAsset address and amount from AssetManager
const { fAssetAddress, amountToBridge } = await getAssetManagerInfo(
BigInt(CONFIG.BRIDGE_LOTS),
);
// 2. Get signer and display account info
const signerAddress = await getSigner(fAssetAddress);
// 3. Get token decimals
const fAssetToken: IERC20MetadataInstance =
await IERC20Metadata.at(fAssetAddress);
const decimals = Number(await fAssetToken.decimals());
console.log("Token decimals:", decimals);
// 4. Prepare bridge parameters
const params = prepareBridgeParams(
signerAddress,
fAssetAddress,
amountToBridge,
decimals,
);
// 5. Check balance and get token contract
const fAsset = await checkBalance(params, decimals);
// 6. Approve tokens and get OFT adapter
const oftAdapter = await approveTokens(
fAsset,
params.amountToBridge,
signerAddress,
fAssetAddress,
decimals,
);
// 7. Encode compose message
const composeMsg = encodeComposeMessage(params);
// 8. Build LayerZero options with compose
const options = buildComposeOptions();
// 9. Build send parameters
const sendParam = buildSendParams(params, composeMsg, options);
// 10. Quote the fee
const nativeFee = await quoteFee(oftAdapter, sendParam);
// 11. Execute the bridge transaction
await executeBridge(oftAdapter, sendParam, nativeFee, signerAddress);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Troubleshooting
Error: HYPERLIQUID_COMPOSER not set
- Solution: Deploy the HyperliquidComposer contract on HyperEVM Testnet first.
Error: Insufficient FTestXRP balance
- Solution: Acquire FTestXRP tokens on Flare Testnet Coston2 faucet or follow our Fasset minting guide.
Error: Insufficient C2FLR for gas
- Solution: Get C2FLR from Flare Testnet Coston2 faucet.
Error: Tokens arrived on HyperEVM but not on HyperCore
- Check that the HyperliquidComposer is correctly configured.
- Verify the compose message was properly encoded.
- Check LayerZero Scan for compose execution status.
Auto-Redeem from Hyperliquid EVM (Step 2)
What It Is
This is Step 2 of the two-step process and requires you to have FXRP tokens on Hyperliquid EVM first (from Step 1).
This script sends FXRP from Hyperliquid EVM Testnet back to Flare Testnet Coston2 with automatic redemption to native XRP on the XRP Ledger.
It uses LayerZero's compose feature to trigger the FAssetRedeemComposer contract upon arrival.
Prerequisites:
- You must have FXRP OFT tokens on Hyperliquid EVM Testnet.
- If you don't have FXRP on Hyperliquid, run
bridgeToHyperEVM.tsfirst (Step 1). - The FAssetRedeemComposer contract must be deployed on Flare Testnet Coston2.
How It Works
Flow Diagram
┌──────────────────┐
│ Developer │
│ (Hyperliquid EVM)│
└────────┬─────────┘
│
│ 1. Has FXRP OFT
▼
┌─────────────────────────┐
│ FXRP OFT (Hyperliquid) │
│ Balance: 10 FXRP │
└────────┬────────────────┘
│
│ 2. Send with Compose Message
│ - Destination: Coston2 Composer
│ - Compose Data: (amount, xrpAddress, redeemer)
▼
┌─────────────────────────┐
│ LayerZero Endpoint │
│ - lzSend() │
│ - Compose enabled │
└────────┬────────────────┘
│
│ 3. Cross-chain message
▼
┌─────────────────────────┐
│ Coston2 Endpoint │
│ - lzReceive() │
│ - lzCompose() │
└────────┬────────────────┘
│
│ 4. Calls Composer
▼
┌─────────────────────────┐
│ FAssetRedeemComposer │
│ - Receives FXRP │
│ - Calculates lots │
│ - Calls AssetManager │
└────────┬────────────────┘
│
│ 5. Redemption
▼
┌─────────────────────────┐
│ FAsset AssetManager │
│ - Burns FXRP │
│ - Releases XRP │
└────────┬────────────────┘
│
│ 6. XRP sent to address
▼
┌─────────────────────────┐
│ XRP Ledger Address │
│ rpHuw4b... │
└─────────────────────────┘
Step-by-Step Process
- Validate Setup:
- Checks that
COSTON2_COMPOSERis configured. - Gets the signer account.
- Checks that
- Connect to FXRP OFT: Gets the OFT contract on Hyperliquid using the
FXRPOFTartifact. - Prepare Redemption Parameters:
- Number of lots to send (default: 1 lot, amount calculated using
calculateAmountToSendutility). - XRP address to receive native XRP.
- Redeemer address (EVM address).
- Number of lots to send (default: 1 lot, amount calculated using
- Encode Compose Message:
- Encodes
(uint256 amount, string xrpAddress, address redeemer). - This tells the composer what to do when tokens arrive.
- Encodes
- Build LayerZero Options:
- Executor gas for
lzReceive(): 1,000,000 - Compose gas for
lzCompose(): 1,000,000
- Executor gas for
- Build Send Parameters:
- Destination: Coston2 (Endpoint ID:
40296). - Recipient: FAssetRedeemComposer contract.
- Amount: Calculated from configured lots (default 1 lot).
- Compose message included
- Destination: Coston2 (Endpoint ID:
- Check Balance: Verifies user has sufficient FXRP OFT.
- Quote Fee: Calculates LayerZero messaging fee.
- Execute Send: Sends FXRP with compose message.
- Auto-Redemption: On arrival, composer automatically redeems to XRP.
Prerequisites
- Network: Must run on Hyperliquid EVM Testnet.
- Balance Requirements:
- FXRP OFT tokens on Hyperliquid (amount you want to redeem).
- HYPE tokens (for gas fees + LayerZero fees).
- Deployed Contracts:
- FAssetRedeemComposer must be deployed on Flare Testnet Coston2.
- Composer address must be set in
.env.
Configuration
Edit the CONFIG object in the script:
const CONFIG = {
HYPERLIQUID_FXRP_OFT:
process.env.HYPERLIQUID_FXRP_OFT ||
"0x14bfb521e318fc3d5e92A8462C65079BC7d4284c",
COSTON2_COMPOSER:
process.env.COSTON2_COMPOSER ||
"0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E",
COSTON2_EID: EndpointId.FLARE_V2_TESTNET,
EXECUTOR_GAS: 1_000_000, // Gas for receiving on Coston2
COMPOSE_GAS: 1_000_000, // Gas for compose execution
SEND_LOTS: "1", // Number of lots to redeem
XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYYVedg1jyPrmp", // Your XRP address
};
How to Run
PREREQUISITE: You must have FXRP tokens on Hyperliquid EVM Testnet before running this script.
If you don't have FXRP on Hyperliquid yet:
- Run
bridgeToHyperEVM.tsfirst (Step 1) - Wait for the bridge transaction to complete (2-5 minutes)
- Verify your FXRP balance on Hyperliquid EVM before proceeding
Once you have FXRP on Hyperliquid:
-
Deploy FAssetRedeemComposer (first time only):
npx hardhat deploy --network coston2 --tags FAssetRedeemComposer -
Configure Environment:
# .env file
HYPERLIQUID_TESTNET_RPC_URL=https://api.hyperliquid-testnet.xyz/evm
DEPLOYER_PRIVATE_KEY=your_private_key_here
COSTON2_COMPOSER=0x... # Required! Set this after deploying the composer
HYPERLIQUID_FXRP_OFT=0x14bfb521e318fc3d5e92A8462C65079BC7d4284c -
Update XRP Address:
- Edit
CONFIG.XRP_ADDRESSin the script to your XRP ledger address - This is where you'll receive the native XRP
- Must be a valid XRP address format (starts with 'r')
- Edit
-
Run the Script on Hyperliquid Testnet:
yarn hardhat run scripts/fassets/autoRedeemFromHyperEVM.ts --network hyperliquidTestnet
Expected Output
Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
✓ Composer configured: 0x123...
📋 Redemption Parameters:
Amount: 10.0 FXRP
XRP Address: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
Redeemer: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Connecting to FXRP OFT on Hyperliquid EVM: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c
✓ Connected to FXRP OFT: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c
OFT address: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c
Compose message encoded
💰 Current FXRP balance: 25.0
Sufficient balance
💵 LayerZero Fee: 0.002456 HYPE
🚀 Sending 10.0 FXRP to Coston2 with auto-redeem...
Target composer: 0x123...
Underlying address: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
✓ Transaction sent: 0xdef456...
Waiting for confirmation...
✅ Confirmed in block: 9876543
🎉 Success! Your FXRP is on the way to Coston2!
📊 Track your cross-chain transaction:
https://testnet.layerzeroscan.com/tx/0xdef456...
⏳ The auto-redeem will execute once the message arrives on Coston2.
XRP will be sent to: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
View autoRedeemFromHyperEVM.ts source code
/**
* Example script to send FXRP from Hyperliquid EVM Testnet to Coston2 with automatic redemption
*
* This demonstrates how to:
* 1. Send OFT tokens from Hyperliquid EVM Testnet (where FXRP is an OFT)
* 2. Use LayerZero compose to trigger automatic redemption on Hyperliquid
* 3. Redeem the underlying asset (XRP) to a specified address
*
* Usage:
* yarn hardhat run scripts/fassets/autoRedeemFromHyperEVM.ts --network hyperliquidTestnet
*/
import { web3 } from "hardhat";
import { formatUnits } from "ethers";
import { Options } from "@layerzerolabs/lz-v2-utilities";
import { EndpointId } from "@layerzerolabs/lz-definitions";
import type { FXRPOFTInstance } from "../../typechain-types";
import { calculateAmountToSend } from "../utils/fassets";
const FXRPOFT = artifacts.require("FXRPOFT");
// Configuration - using existing deployed contracts
const CONFIG = {
HYPERLIQUID_FXRP_OFT:
process.env.HYPERLIQUID_FXRP_OFT ||
"0x14bfb521e318fc3d5e92A8462C65079BC7d4284c",
COSTON2_COMPOSER:
process.env.COSTON2_COMPOSER ||
"0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E",
COSTON2_EID: EndpointId.FLARE_V2_TESTNET,
EXECUTOR_GAS: 1_000_000,
COMPOSE_GAS: 1_000_000,
SEND_LOTS: "1",
XRP_ADDRESS: "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp",
} as const;
type RedemptionParams = {
amountToSend: bigint;
underlyingAddress: string;
redeemer: string;
signerAddress: string;
executor: string;
};
type SendParams = {
dstEid: EndpointId;
to: string;
amountLD: string;
minAmountLD: string;
extraOptions: string;
composeMsg: string;
oftCmd: string;
};
/**
* Gets the signer and validates composer is deployed
*/
async function validateSetup() {
const accounts = await web3.eth.getAccounts();
const signerAddress = accounts[0];
console.log("Using account:", signerAddress);
if (!CONFIG.COSTON2_COMPOSER) {
throw new Error(
"HYPERLIQUID_COMPOSER not set in .env!\n" +
" Deploy FAssetRedeemComposer first on Hyperliquid:\n" +
" npx hardhat deploy --network hyperliquid --tags FAssetRedeemComposer",
);
}
console.log("✓ Composer configured:", CONFIG.COSTON2_COMPOSER);
return signerAddress;
}
/**
* Prepares redemption parameters
*/
function prepareRedemptionParams(
signerAddress: string,
decimals: number,
): RedemptionParams {
const amountToSend = calculateAmountToSend(BigInt(CONFIG.SEND_LOTS));
const underlyingAddress = CONFIG.XRP_ADDRESS;
const redeemer = signerAddress;
console.log("\n📋 Redemption Parameters:");
console.log(
"Amount:",
formatUnits(amountToSend.toString(), decimals),
"FXRP",
);
console.log("XRP Address:", underlyingAddress);
console.log("Redeemer:", redeemer);
const executor = "0x0000000000000000000000000000000000000000";
return { amountToSend, underlyingAddress, redeemer, signerAddress, executor };
}
/**
* Connects to the OFT contract on Hyperliquid EVM
*/
async function connectToOFT(): Promise<FXRPOFTInstance> {
console.log(
"Connecting to FXRP OFT on Hyperliquid EVM:",
CONFIG.HYPERLIQUID_FXRP_OFT,
);
const oft = await FXRPOFT.at(CONFIG.HYPERLIQUID_FXRP_OFT);
console.log("\n✓ Connected to FXRP OFT:", CONFIG.HYPERLIQUID_FXRP_OFT);
console.log("OFT address:", oft.address);
return oft;
}
/**
* Encodes the compose message with redemption details
* Format: (amountToRedeem, underlyingAddress, redeemer)
*/
function encodeComposeMessage(params: RedemptionParams): string {
// redeem(uint256 _lots, string memory _redeemerUnderlyingAddressString, executor address)
const composeMsg = web3.eth.abi.encodeParameters(
["uint256", "string", "address"],
[params.amountToSend.toString(), params.underlyingAddress, params.redeemer],
);
console.log("Compose message encoded");
return composeMsg;
}
/**
* Builds LayerZero options with compose support
*/
function buildComposeOptions(): string {
const options = Options.newOptions()
.addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0)
.addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0);
return options.toHex();
}
/**
* Builds the send parameters for LayerZero
*/
function buildSendParams(
params: RedemptionParams,
composeMsg: string,
options: string,
): SendParams {
return {
dstEid: CONFIG.COSTON2_EID,
to: web3.utils.padLeft(CONFIG.COSTON2_COMPOSER, 64),
amountLD: params.amountToSend.toString(),
minAmountLD: params.amountToSend.toString(),
extraOptions: options,
composeMsg: composeMsg,
oftCmd: "0x",
};
}
/**
* Checks if user has sufficient FXRP balance
*/
async function checkBalance(
oft: FXRPOFTInstance,
signerAddress: string,
amountToSend: bigint,
decimals: number,
): Promise<void> {
const balance = await oft.balanceOf(signerAddress);
console.log(
"\n💰 Current FXRP balance:",
formatUnits(balance.toString(), decimals),
);
if (BigInt(balance.toString()) < amountToSend) {
console.error("\n❌ Insufficient FXRP balance!");
console.log(
" Required:",
formatUnits(amountToSend.toString(), decimals),
"FXRP",
);
console.log(
" Available:",
formatUnits(balance.toString(), decimals),
"FXRP",
);
throw new Error("Insufficient FXRP balance");
}
console.log("Sufficient balance");
}
/**
* Quotes the LayerZero fee for the send transaction
*/
async function quoteFee(oft: FXRPOFTInstance, sendParam: SendParams) {
const result = await oft.quoteSend(sendParam, false);
const nativeFee = BigInt(result.nativeFee.toString());
const lzTokenFee = BigInt(result.lzTokenFee.toString());
console.log(
"\n💵 LayerZero Fee:",
formatUnits(nativeFee.toString(), 18),
"HYPE",
);
return { nativeFee, lzTokenFee };
}
/**
* Executes the send with auto-redeem
*/
async function executeSendAndRedeem(
oft: FXRPOFTInstance,
sendParam: SendParams,
nativeFee: bigint,
lzTokenFee: bigint,
params: RedemptionParams,
decimals: number,
): Promise<void> {
console.log(
"\n🚀 Sending",
formatUnits(params.amountToSend.toString(), decimals),
"FXRP to Coston2 with auto-redeem...",
);
console.log("Target composer:", CONFIG.COSTON2_COMPOSER);
console.log("Underlying address:", params.underlyingAddress);
const tx = await oft.send(
sendParam,
{ nativeFee: nativeFee.toString(), lzTokenFee: lzTokenFee.toString() },
params.signerAddress,
{ value: nativeFee.toString() },
);
console.log("\n✓ Transaction sent:", tx.tx);
console.log("✅ Confirmed in block:", tx.receipt.blockNumber);
console.log("\n🎉 Success! Your FXRP is on the way to Coston2!");
console.log("\n📊 Track your cross-chain transaction:");
console.log(`https://testnet.layerzeroscan.com/tx/${tx.tx}`);
console.log(
"\n⏳ The auto-redeem will execute once the message arrives on Coston2.",
);
console.log("XRP will be sent to:", params.underlyingAddress);
}
async function main() {
// 1. Validate setup and get signer
const signerAddress = await validateSetup();
// 2. Connect to OFT contract
const oft = await connectToOFT();
// 3. Get token decimals
const decimals = Number(await oft.decimals());
console.log("Token decimals:", decimals);
// 4. Prepare redemption parameters
const params = prepareRedemptionParams(signerAddress, decimals);
// 5. Encode compose message
const composeMsg = encodeComposeMessage(params);
// 6. Build LayerZero options
const options = buildComposeOptions();
// 7. Build send parameters
const sendParam = buildSendParams(params, composeMsg, options);
// 8. Check balance
await checkBalance(oft, params.signerAddress, params.amountToSend, decimals);
// 9. Quote fee
const { nativeFee, lzTokenFee } = await quoteFee(oft, sendParam);
// 10. Execute send with auto-redeem
await executeSendAndRedeem(
oft,
sendParam,
nativeFee,
lzTokenFee,
params,
decimals,
);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Auto-Redeem from HyperCore
What It Is
This script provides a complete auto-redemption flow starting from HyperCore (Hyperliquid's spot trading layer) to native XRP on the XRP Ledger. Unlike the HyperEVM flow which assumes you already have FXRP on HyperEVM, this script handles the full journey: HyperCore → HyperEVM → Coston2 → XRP Ledger.
HyperCore is Hyperliquid's high-performance spot trading layer where tokens are held in spot wallets. HyperEVM is Hyperliquid's EVM-compatible execution environment where tokens exist as ERC-20s. Tokens must be transferred from HyperCore to HyperEVM before they can be bridged via LayerZero.
Prerequisites:
- You must have FXRP tokens in your HyperCore spot wallet.
- You need HYPE tokens on HyperEVM for LayerZero fees.
- The FAssetRedeemComposer contract must be deployed on Flare Testnet Coston2.
How It Works
Flow Diagram
┌─────────────────────────┐
│ Developer │
│ (HyperCore Spot) │
└────────┬────────────────┘
│
│ 1. Has FXRP in spot wallet
▼
┌─────────────────────────┐
│ HyperCore Spot Wallet │
│ Balance: 10 FXRP │
└────────┬────────────────┘
│
│ 2. spotSend to system address
│ (EIP-712 signed)
▼
┌─────────────────────────┐
│ Hyperliquid API │
│ - /exchange endpoint │
│ - spotSend action │
└────────┬────────────────┘
│
│ 3. Tokens appear on HyperEVM
▼
┌─────────────────────────┐
│ FXRP OFT (HyperEVM) │
│ Balance: 10 FXRP │
└────────┬────────────────┘
│
│ 4. Send with Compose Message
│ - Destination: Coston2 Composer
│ - Compose Data: (amount, xrpAddress, redeemer)
▼
┌─────────────────────────┐
│ LayerZero Endpoint │
│ - lzSend() │
│ - Compose enabled │
└────────┬────────────────┘
│
│ 5. Cross-chain message
▼
┌─────────────────────────┐
│ Coston2 Endpoint │
│ - lzReceive() │
│ - lzCompose() │
└────────┬────────────────┘
│
│ 6. Calls Composer
▼
┌─────────────────────────┐
│ FAssetRedeemComposer │
│ - Receives FXRP │
│ - Calculates lots │
│ - Calls AssetManager │
└────────┬────────────────┘
│
│ 7. Redemption
▼
┌─────────────────────────┐
│ FAsset AssetManager │
│ - Burns FXRP │
│ - Releases XRP │
└────────┬────────────────┘
│
│ 8. XRP sent to address
▼
┌─────────────────────────┐
│ XRP Ledger Address │
│ rpHuw4b... │
└─────────────────────────┘
Step-by-Step Process
- Validate Setup:
- Checks that
COSTON2_COMPOSERis configured. - Gets the signer account.
- Checks that
- Check HyperCore Balance:
- Queries HyperCore spot balance via the
/infoAPI endpoint. - Verifies sufficient FXRP is available.
- Queries HyperCore spot balance via the
- Transfer from HyperCore to HyperEVM:
- Prepares an EIP-712 signed
spotSendmessage. - Sends to the FXRP system address (
0x20000000000000000000000000000000000005a3). - Waits for the transfer to settle on HyperEVM.
- Prepares an EIP-712 signed
- Connect to FXRP OFT: Gets the OFT contract on HyperEVM.
- Prepare Redemption Parameters:
- Number of lots to send (default: 1 lot).
- XRP address to receive native XRP.
- Redeemer address (EVM address).
- Check HyperEVM Balance: Verifies tokens arrived from HyperCore.
- Encode Compose Message:
- Encodes
(uint256 amount, string xrpAddress, address redeemer). - This tells the composer what to do when tokens arrive.
- Encodes
- Build LayerZero Options:
- Executor gas for
lzReceive(): 1,000,000 - Compose gas for
lzCompose(): 1,000,000
- Executor gas for
- Quote Fee: Calculates LayerZero messaging fee.
- Execute Send: Sends FXRP with compose message to Coston2.
- Auto-Redemption: On arrival, composer automatically redeems to XRP.
Understanding HyperCore Transfers
The script uses Hyperliquid's spotSend API to transfer tokens from HyperCore to HyperEVM.
This requires EIP-712 signing with specific parameters.
EIP-712 Domain
const EIP712_DOMAIN = {
name: "HyperliquidSignTransaction",
version: "1",
chainId: 42161, // Arbitrum chainId - required by Hyperliquid API
verifyingContract: "0x0000000000000000000000000000000000000000",
};
Hyperliquid's API requires Arbitrum's chainId (42161) for all EIP-712 signatures, regardless of the actual chain. This is a legacy requirement from when Hyperliquid settled on Arbitrum.
System Address
To transfer tokens from HyperCore to HyperEVM, you send them to a special system address. Each token has a unique system address based on its token index.
// System address for FXRP on testnet (token index 1443 = 0x5A3)
FXRP_SYSTEM_ADDRESS: "0x20000000000000000000000000000000000005a3";
Prerequisites
- Network: Must run on Hyperliquid EVM Testnet.
- Balance Requirements:
- FXRP tokens in your HyperCore spot wallet (amount you want to redeem).
- HYPE tokens on HyperEVM (for gas fees + LayerZero fees).
- Deployed Contracts:
- FAssetRedeemComposer must be deployed on Flare Testnet Coston2.
- Composer address must be set in
.env.
Configuration
Edit the CONFIG object in the script:
const CONFIG = {
// Testnet config
HYPERLIQUID_API:
process.env.HYPERLIQUID_API || "https://api.hyperliquid-testnet.xyz",
HYPERLIQUID_FXRP_OFT:
process.env.HYPERLIQUID_FXRP_OFT ||
"0x14bfb521e318fc3d5e92A8462C65079BC7d4284c",
// System address for FXRP on testnet (token index 1443 = 0x5A3)
FXRP_SYSTEM_ADDRESS: "0x20000000000000000000000000000000000005a3",
FXRP_TOKEN_ID: "FXRP:0x2af78df5b575b45eea8a6a1175026dd6",
COSTON2_COMPOSER:
process.env.COSTON2_COMPOSER ||
"0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E",
COSTON2_EID: EndpointId.FLARE_V2_TESTNET,
EXECUTOR_GAS: 1_000_000, // Gas for receiving on Coston2
COMPOSE_GAS: 1_000_000, // Gas for compose execution
SEND_LOTS: "1", // Number of lots to redeem
XRP_ADDRESS: process.env.XRP_ADDRESS || "rpHuw4bKSjonKRrKKVYYVedg1jyPrmp",
HYPERLIQUID_CHAIN: "Testnet",
};
How to Run
PREREQUISITE: You must have FXRP tokens in your HyperCore spot wallet before running this script.
-
Deploy FAssetRedeemComposer (first time only):
npx hardhat deploy --network coston2 --tags FAssetRedeemComposer -
Configure Environment:
# .env file
HYPERLIQUID_TESTNET_RPC_URL=https://api.hyperliquid-testnet.xyz/evm
DEPLOYER_PRIVATE_KEY=your_private_key_here
COSTON2_COMPOSER=0x... # Required! Set this after deploying the composer
HYPERLIQUID_FXRP_OFT=0x14bfb521e318fc3d5e92A8462C65079BC7d4284c
XRP_ADDRESS=rYourXRPAddressHere # Your XRP ledger address -
Ensure you have FXRP on HyperCore:
- Bridge FXRP to HyperCore using
bridgeToHyperEVM.tsfollowed by a transfer to HyperCore. - Or acquire FXRP on Hyperliquid's spot market.
- Bridge FXRP to HyperCore using
-
Run the Script on Hyperliquid Testnet:
yarn hardhat run scripts/fassets/autoRedeemFromHyperCore.ts --network hyperliquidTestnet
Expected Output
============================================================
FXRP Auto-Redemption from HyperCore
HyperCore → HyperEVM → Coston2 → XRP Ledger
============================================================
Using account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
✓ Coston2 Composer configured: 0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E
📊 Checking HyperCore spot balance...
HyperCore FXRP balance: 25.0
📤 Step 1: Transferring FXRP from HyperCore to HyperEVM...
Amount: 10 FXRP
Destination (system address): 0x20000000000000000000000000000000000005a3
✅ HyperCore → HyperEVM transfer initiated
Response: {"status":"ok","response":{"type":"default"}}
Waiting for transfer to settle on HyperEVM...
Connecting to FXRP OFT on HyperEVM: 0x14bfb521e318fc3d5e92A8462C65079BC7d4284c
✓ Connected to FXRP OFT
Token decimals: 6
📋 Redemption Parameters:
Amount: 10.0 FXRP
XRP Address: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
Redeemer: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
💰 HyperEVM FXRP balance: 10.0
✓ Sufficient balance on HyperEVM
Compose message encoded for auto-redemption
💵 LayerZero Fee: 0.003456 HYPE
📤 Step 2: Sending FXRP from HyperEVM to Coston2 with auto-redeem...
Amount: 10.0 FXRP
Target composer: 0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E
XRP destination: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
✓ Transaction sent: 0xabc123...
✅ Confirmed in block: 9876543
🎉 Success! Auto-redemption initiated!
📊 Track your cross-chain transaction:
https://testnet.layerzeroscan.com/tx/0xabc123...
⏳ The auto-redeem will execute once the message arrives on Coston2.
XRP will be sent to: rpHuw4bKSjonKRrKKVYYVedg1jyPrmp
View autoRedeemFromHyperCore.ts source code
/**
* Auto-redeem FXRP from Hyperliquid HyperCore to native XRP
*
* This script demonstrates the complete flow:
* 1. Transfer FXRP from HyperCore spot to HyperEVM (via Hyperliquid spotSend API)
* 2. Send FXRP from HyperEVM to Coston2 via LayerZero with compose
* 3. FAssetRedeemComposer on Coston2 automatically redeems to native XRP
*
* Prerequisites:
* - FXRP tokens on HyperCore spot wallet
* - HYPE on HyperEVM for LayerZero fees
* - FAssetRedeemComposer deployed on Coston2
*
* Usage:
* yarn hardhat run scripts/fassets/autoRedeemFromHyperCore.ts --network hyperliquidTestnet
*/
import { web3 } from "hardhat";
import { formatUnits } from "ethers";
import { Options } from "@layerzerolabs/lz-v2-utilities";
import { EndpointId } from "@layerzerolabs/lz-definitions";
import type { FXRPOFTInstance } from "../../typechain-types";
import { calculateAmountToSend } from "../utils/fassets";
const FXRPOFT = artifacts.require("FXRPOFT");
// Configuration
const CONFIG = {
// Testnet config
HYPERLIQUID_API:
process.env.HYPERLIQUID_API || "https://api.hyperliquid-testnet.xyz",
HYPERLIQUID_FXRP_OFT:
process.env.HYPERLIQUID_FXRP_OFT ||
"0x14bfb521e318fc3d5e92A8462C65079BC7d4284c",
// System address for FXRP on testnet (token index 1443 = 0x5A3)
FXRP_SYSTEM_ADDRESS: "0x20000000000000000000000000000000000005a3",
FXRP_TOKEN_ID: "FXRP:0x2af78df5b575b45eea8a6a1175026dd6",
COSTON2_COMPOSER:
process.env.COSTON2_COMPOSER ||
"0x5051E8db650E9e0E2a3f03010Ee5c60e79CF583E",
COSTON2_EID: EndpointId.FLARE_V2_TESTNET,
EXECUTOR_GAS: 1_000_000,
COMPOSE_GAS: 1_000_000,
SEND_LOTS: "1",
XRP_ADDRESS: process.env.XRP_ADDRESS || "rpHuw4bKSjonKRrKKVYUZYYVedg1jyPrmp",
HYPERLIQUID_CHAIN: "Testnet",
} as const;
type RedemptionParams = {
amountToSend: bigint;
underlyingAddress: string;
redeemer: string;
signerAddress: string;
};
type SendParams = {
dstEid: EndpointId;
to: string;
amountLD: string;
minAmountLD: string;
extraOptions: string;
composeMsg: string;
oftCmd: string;
};
/**
* EIP-712 domain for Hyperliquid signing.
* Note: Hyperliquid's API requires Arbitrum's chainId (42161) for all EIP-712 signatures,
* regardless of the actual chain. This is a legacy requirement from when Hyperliquid settled on Arbitrum.
*/
const EIP712_DOMAIN = {
name: "HyperliquidSignTransaction",
version: "1",
chainId: 42161,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
/**
* EIP-712 types for spotSend action
*/
const SPOT_SEND_TYPES = {
"HyperliquidTransaction:SpotSend": [
{ name: "hyperliquidChain", type: "string" },
{ name: "destination", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "time", type: "uint64" },
],
};
/**
* Validates the setup
*/
async function validateSetup() {
const accounts = await web3.eth.getAccounts();
const signerAddress = accounts[0];
console.log("Using account:", signerAddress);
if (!CONFIG.COSTON2_COMPOSER) {
throw new Error(
"COSTON2_COMPOSER not set in .env!\n" +
" Deploy FAssetRedeemComposer first on Coston2:\n" +
" yarn hardhat run scripts/fassets/deployFassetRedeemComposer.ts --network coston2",
);
}
console.log("✓ Coston2 Composer configured:", CONFIG.COSTON2_COMPOSER);
return signerAddress;
}
/**
* Queries HyperCore spot balance for FXRP
*/
async function getHyperCoreBalance(address: string): Promise<string> {
const response = await fetch(CONFIG.HYPERLIQUID_API + "/info", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "spotClearinghouseState",
user: address,
}),
});
const data = await response.json();
const balances = data.balances || [];
// Find FXRP balance
const fxrpBalance = balances.find(
(b: { coin: string; hold: string }) => b.coin === "FXRP",
);
return fxrpBalance ? fxrpBalance.hold : "0";
}
/**
* Transfers FXRP from HyperCore to HyperEVM using spotSend API
*/
async function transferFromHyperCoreToHyperEVM(
signerAddress: string,
amount: string,
): Promise<void> {
console.log("\n📤 Step 1: Transferring FXRP from HyperCore to HyperEVM...");
console.log(" Amount:", amount, "FXRP");
console.log(" Destination (system address):", CONFIG.FXRP_SYSTEM_ADDRESS);
const timestamp = Date.now();
// Prepare the message for EIP-712 signing
const message = {
hyperliquidChain: CONFIG.HYPERLIQUID_CHAIN,
destination: CONFIG.FXRP_SYSTEM_ADDRESS,
token: CONFIG.FXRP_TOKEN_ID,
amount: amount,
time: timestamp,
};
// Sign with EIP-712 using web3
const typedData = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
...SPOT_SEND_TYPES,
},
primaryType: "HyperliquidTransaction:SpotSend" as const,
domain: EIP712_DOMAIN,
message: message,
};
const signature = await web3.eth.signTypedData(signerAddress, typedData);
// Prepare the action payload
const action = {
type: "spotSend",
hyperliquidChain: CONFIG.HYPERLIQUID_CHAIN,
signatureChainId: "0xa4b1", // Arbitrum chainId (42161) in hex - required by Hyperliquid API
destination: CONFIG.FXRP_SYSTEM_ADDRESS.toLowerCase(),
token: CONFIG.FXRP_TOKEN_ID,
amount: amount,
time: timestamp,
};
// Send to Hyperliquid exchange API
const response = await fetch(CONFIG.HYPERLIQUID_API + "/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: action,
nonce: timestamp,
signature: signature,
}),
});
const result = await response.json();
if (result.status === "err") {
throw new Error(`HyperCore transfer failed: ${result.response}`);
}
console.log("✅ HyperCore → HyperEVM transfer initiated");
console.log(" Response:", JSON.stringify(result));
// Wait for transfer to complete (typically ~2 seconds for HyperEVM block)
console.log(" Waiting for transfer to settle on HyperEVM...");
await new Promise((resolve) => setTimeout(resolve, 5000));
}
/**
* Prepares redemption parameters
*/
function prepareRedemptionParams(
signerAddress: string,
decimals: number,
): RedemptionParams {
const amountToSend = calculateAmountToSend(BigInt(CONFIG.SEND_LOTS));
const underlyingAddress = CONFIG.XRP_ADDRESS;
const redeemer = signerAddress;
console.log("\n📋 Redemption Parameters:");
console.log(
"Amount:",
formatUnits(amountToSend.toString(), decimals),
"FXRP",
);
console.log("XRP Address:", underlyingAddress);
console.log("Redeemer:", redeemer);
return { amountToSend, underlyingAddress, redeemer, signerAddress };
}
/**
* Connects to the OFT contract on HyperEVM
*/
async function connectToOFT(): Promise<FXRPOFTInstance> {
console.log(
"\nConnecting to FXRP OFT on HyperEVM:",
CONFIG.HYPERLIQUID_FXRP_OFT,
);
const oft = await FXRPOFT.at(CONFIG.HYPERLIQUID_FXRP_OFT);
console.log("✓ Connected to FXRP OFT");
return oft;
}
/**
* Encodes the compose message for auto-redemption
*/
function encodeComposeMessage(params: RedemptionParams): string {
const composeMsg = web3.eth.abi.encodeParameters(
["uint256", "string", "address"],
[params.amountToSend.toString(), params.underlyingAddress, params.redeemer],
);
console.log("Compose message encoded for auto-redemption");
return composeMsg;
}
/**
* Builds LayerZero options with compose
*/
function buildComposeOptions(): string {
const options = Options.newOptions()
.addExecutorLzReceiveOption(CONFIG.EXECUTOR_GAS, 0)
.addExecutorComposeOption(0, CONFIG.COMPOSE_GAS, 0);
return options.toHex();
}
/**
* Builds send parameters for LayerZero
*/
function buildSendParams(
params: RedemptionParams,
composeMsg: string,
options: string,
): SendParams {
return {
dstEid: CONFIG.COSTON2_EID,
to: web3.utils.padLeft(CONFIG.COSTON2_COMPOSER, 64),
amountLD: params.amountToSend.toString(),
minAmountLD: params.amountToSend.toString(),
extraOptions: options,
composeMsg: composeMsg,
oftCmd: "0x",
};
}
/**
* Checks HyperEVM balance
*/
async function checkHyperEVMBalance(
oft: FXRPOFTInstance,
signerAddress: string,
amountToSend: bigint,
decimals: number,
): Promise<void> {
const balance = await oft.balanceOf(signerAddress);
console.log(
"\n💰 HyperEVM FXRP balance:",
formatUnits(balance.toString(), decimals),
);
if (BigInt(balance.toString()) < amountToSend) {
console.error("\n❌ Insufficient FXRP balance on HyperEVM!");
console.log(
" Required:",
formatUnits(amountToSend.toString(), decimals),
"FXRP",
);
console.log(
" Available:",
formatUnits(balance.toString(), decimals),
"FXRP",
);
throw new Error(
"Insufficient FXRP balance on HyperEVM. Transfer from HyperCore may still be pending.",
);
}
console.log("✓ Sufficient balance on HyperEVM");
}
/**
* Quotes LayerZero fee
*/
async function quoteFee(oft: FXRPOFTInstance, sendParam: SendParams) {
const result = await oft.quoteSend(sendParam, false);
const nativeFee = BigInt(result.nativeFee.toString());
const lzTokenFee = BigInt(result.lzTokenFee.toString());
console.log(
"\n💵 LayerZero Fee:",
formatUnits(nativeFee.toString(), 18),
"HYPE",
);
return { nativeFee, lzTokenFee };
}
/**
* Executes LayerZero send with auto-redeem
*/
async function executeSendAndRedeem(
oft: FXRPOFTInstance,
sendParam: SendParams,
nativeFee: bigint,
lzTokenFee: bigint,
params: RedemptionParams,
decimals: number,
): Promise<void> {
console.log(
"\n📤 Step 2: Sending FXRP from HyperEVM to Coston2 with auto-redeem...",
);
console.log(
" Amount:",
formatUnits(params.amountToSend.toString(), decimals),
"FXRP",
);
console.log(" Target composer:", CONFIG.COSTON2_COMPOSER);
console.log(" XRP destination:", params.underlyingAddress);
const tx = await oft.send(
sendParam,
{ nativeFee: nativeFee.toString(), lzTokenFee: lzTokenFee.toString() },
params.signerAddress,
{ value: nativeFee.toString() },
);
console.log("\n✓ Transaction sent:", tx.tx);
console.log("✅ Confirmed in block:", tx.receipt.blockNumber);
console.log("\n🎉 Success! Auto-redemption initiated!");
console.log("\n📊 Track your cross-chain transaction:");
console.log(`https://testnet.layerzeroscan.com/tx/${tx.tx}`);
console.log(
"\n⏳ The auto-redeem will execute once the message arrives on Coston2.",
);
console.log("XRP will be sent to:", params.underlyingAddress);
}
async function main() {
console.log("=".repeat(60));
console.log("FXRP Auto-Redemption from HyperCore");
console.log("HyperCore → HyperEVM → Coston2 → XRP Ledger");
console.log("=".repeat(60));
// 1. Validate setup
const signerAddress = await validateSetup();
// 2. Check HyperCore balance
console.log("\n📊 Checking HyperCore spot balance...");
const hyperCoreBalance = await getHyperCoreBalance(signerAddress);
console.log(" HyperCore FXRP balance:", hyperCoreBalance);
const lotSize = 10; // 1 lot = 10 FXRP
const requiredAmount = parseInt(CONFIG.SEND_LOTS) * lotSize;
if (parseFloat(hyperCoreBalance) < requiredAmount) {
throw new Error(
`Insufficient FXRP on HyperCore. Have: ${hyperCoreBalance}, Need: ${requiredAmount}\n` +
"Bridge FXRP to HyperCore first using bridgeToHyperCore.ts",
);
}
// 3. Transfer from HyperCore to HyperEVM
await transferFromHyperCoreToHyperEVM(
signerAddress,
requiredAmount.toString(),
);
// 4. Connect to OFT on HyperEVM
const oft = await connectToOFT();
// 5. Get token decimals
const decimals = Number(await oft.decimals());
console.log("Token decimals:", decimals);
// 6. Prepare redemption parameters
const params = prepareRedemptionParams(signerAddress, decimals);
// 7. Check HyperEVM balance (should have tokens now)
await checkHyperEVMBalance(
oft,
params.signerAddress,
params.amountToSend,
decimals,
);
// 8. Encode compose message
const composeMsg = encodeComposeMessage(params);
// 9. Build LayerZero options
const options = buildComposeOptions();
// 10. Build send parameters
const sendParam = buildSendParams(params, composeMsg, options);
// 11. Quote fee
const { nativeFee, lzTokenFee } = await quoteFee(oft, sendParam);
// 12. Execute send with auto-redeem
await executeSendAndRedeem(
oft,
sendParam,
nativeFee,
lzTokenFee,
params,
decimals,
);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Troubleshooting
Error: Insufficient FXRP on HyperCore
- Solution: Bridge FXRP to HyperCore first or acquire FXRP on Hyperliquid's spot market.
Error: HyperCore transfer failed
- Check that your wallet is properly configured with Hyperliquid.
- Verify the EIP-712 signature is being generated correctly.
- Ensure the FXRP_TOKEN_ID matches the correct token on the network.
Error: Insufficient FXRP balance on HyperEVM
- The transfer from HyperCore may still be pending.
- Wait a few more seconds and try again.
- Check your HyperEVM balance directly.
Error: Insufficient HYPE for gas
- Get HYPE tokens from Hyperliquid testnet faucet.
FAQ
Q: How long does bridging take? A: Typically 2-5 minutes for LayerZero message delivery + execution time.
Q: What's the minimum amount I can bridge? A: Any amount for bridging to Hyperliquid. For auto-redeem, minimum is 1 lot (10 FXRP for XRP).
Q: Can I bridge to a different address?
A: Yes, edit the recipientAddress parameter in the scripts.
Q: What happens if compose execution fails?
A: Tokens will be stuck in the composer. Owner can recover using recoverTokens().
Q: Can I use this on mainnet? A: This is designed for testnet. For mainnet, update contract addresses, thoroughly test, and audit all code.
Q: How do I get FTestXRP on Flare Testnet Coston2? A: Use Flare's FAsset minting process via the AssetManager contract.
Q: What if I don't have HYPE tokens? A: Get them from Hyperliquid testnet faucet or DEX.
Q: What's the difference between HyperCore and HyperEVM? A: HyperCore is Hyperliquid's high-performance spot trading layer where tokens are held in spot wallets. HyperEVM is Hyperliquid's EVM-compatible execution environment where tokens exist as ERC-20s. You need to transfer tokens from HyperCore to HyperEVM before bridging via LayerZero.
Q: Why does the HyperCore transfer use Arbitrum's chainId? A: Hyperliquid's API requires Arbitrum's chainId (42161) for all EIP-712 signatures. This is a legacy requirement from when Hyperliquid settled on Arbitrum.
Discovering Available Bridge Routes
The getOftPeers.ts utility script discovers all configured LayerZero peers for the FXRP OFT Adapter on Flare Testnet Coston2.
It scans all LayerZero V2 testnet endpoints to find which EVM chains have been configured as valid bridge destinations.
Before bridging FXRP to another chain, you need to know which chains are supported. This script:
- Automatically discovers all configured peer addresses
- Shows which EVM chains you can bridge FXRP to/from
- Provides the peer contract addresses for each chain
- Outputs results in both human-readable and JSON formats
How It Works
- Loads V2 Testnet Endpoints: Dynamically retrieves all LayerZero V2 testnet endpoint IDs from the
@layerzerolabs/lz-definitionspackage. - Queries Peers: For each endpoint, calls the
peers()function on the OFT Adapter contract. - Filters Results: Only shows endpoints that have a non-zero peer address configured.
- Formats Output: Displays results in a table format and as JSON for programmatic use.
How to Run
yarn hardhat run scripts/layerzero/getOFTPeers.ts --network coston2
Expected Output
=== FXRP OFT Adapter Peers Discovery ===
OFT Adapter: 0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639
Network: Coston2 (Flare Testnet)
Scanning 221 LayerZero V2 Testnet endpoints...
✅ Bsc (EID: 40102): 0xac7c4a07670589cf83b134a843bfe86c45a4bf4e
✅ Sepolia (EID: 40161): 0x81672c5d42f3573ad95a0bdfbe824faac547d4e6
✅ Hyperliquid (EID: 40362): 0x14bfb521e318fc3d5e92a8462c65079bc7d4284c
============================================================
SUMMARY: Configured Peers
============================================================
Found 3 configured peer(s):
| Chain | EID | Peer Address |
|-------|-----|--------------|
| Bsc | 40102 | 0xac7c4a07670589cf83b134a843bfe86c45a4bf4e |
| Sepolia | 40161 | 0x81672c5d42f3573ad95a0bdfbe824faac547d4e6 |
| Hyperliquid | 40362 | 0x14bfb521e318fc3d5e92a8462c65079bc7d4284c |
--- Available Routes ---
You can bridge FXRP to/from the following chains:
• Bsc
• Sepolia
• Hyperliquid
Once you've identified available peers, you can:
- Bridge to any discovered chain: Update the destination EID in
bridgeToHyperEVM.tsto target a different chain - Verify peer addresses: Use the peer addresses to interact with OFT contracts on other chains
- Build integrations: Use the JSON output to programmatically determine available routes
View getOftPeers.ts source code
/**
* Get all LayerZero peers for the FXRP OFT Adapter on Coston2
*
* Usage:
* yarn hardhat run scripts/layerzero/getOFTPeers.ts --network coston2
*/
import { web3 } from "hardhat";
import { EndpointId } from "@layerzerolabs/lz-definitions";
// FXRP OFT Adapter on Coston2
const OFT_ADAPTER_ADDRESS = "0xCd3d2127935Ae82Af54Fc31cCD9D3440dbF46639";
// Minimal OApp ABI for peers function
const OAPP_ABI = [
{
inputs: [{ internalType: "uint32", name: "eid", type: "uint32" }],
name: "peers",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
];
// Get ALL V2 Testnet endpoints dynamically from the EndpointId enum
function getAllV2TestnetEndpoints(): { name: string; eid: number }[] {
const endpoints: { name: string; eid: number }[] = [];
for (const [key, value] of Object.entries(EndpointId)) {
// Only include V2 testnet endpoints (they end with _V2_TESTNET and have numeric values)
if (key.endsWith("_V2_TESTNET") && typeof value === "number") {
// Convert key like "SEPOLIA_V2_TESTNET" to "Sepolia"
const name = key
.replace("_V2_TESTNET", "")
.split("_")
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
.join(" ");
endpoints.push({ name, eid: value });
}
}
// Sort by EID for consistent output
return endpoints.sort((a, b) => a.eid - b.eid);
}
const V2_TESTNET_ENDPOINTS = getAllV2TestnetEndpoints();
const ZERO_BYTES32 =
"0x0000000000000000000000000000000000000000000000000000000000000000";
async function main() {
console.log("=== FXRP OFT Adapter Peers Discovery ===\n");
console.log(`OFT Adapter: ${OFT_ADAPTER_ADDRESS}`);
console.log(`Network: Coston2 (Flare Testnet)\n`);
const oftAdapter = new web3.eth.Contract(OAPP_ABI, OFT_ADAPTER_ADDRESS);
const configuredPeers: { name: string; eid: number; peer: string }[] = [];
const errors: { name: string; eid: number; error: string }[] = [];
console.log(
`Scanning ${V2_TESTNET_ENDPOINTS.length} LayerZero V2 Testnet endpoints...\n`,
);
for (const endpoint of V2_TESTNET_ENDPOINTS) {
try {
const peer = await oftAdapter.methods.peers(endpoint.eid).call();
if (peer && peer !== ZERO_BYTES32) {
// Convert bytes32 to address (last 20 bytes)
const peerAddress = "0x" + peer.slice(-40);
configuredPeers.push({
name: endpoint.name,
eid: endpoint.eid,
peer: peerAddress,
});
console.log(
`✅ ${endpoint.name} (EID: ${endpoint.eid}): ${peerAddress}`,
);
}
} catch (error: unknown) {
// Some endpoints might not exist or the contract might revert
const errorMessage =
error instanceof Error ? error.message.slice(0, 50) : "Unknown error";
errors.push({
name: endpoint.name,
eid: endpoint.eid,
error: errorMessage,
});
}
}
console.log("\n" + "=".repeat(60));
console.log("SUMMARY: Configured Peers");
console.log("=".repeat(60) + "\n");
if (configuredPeers.length === 0) {
console.log("No peers configured for the FXRP OFT Adapter.\n");
} else {
console.log(`Found ${configuredPeers.length} configured peer(s):\n`);
console.log("| Chain | EID | Peer Address |");
console.log("|-------|-----|--------------|");
for (const peer of configuredPeers) {
console.log(`| ${peer.name} | ${peer.eid} | ${peer.peer} |`);
}
console.log("\n--- Available Routes ---");
console.log("You can bridge FXRP to/from the following chains:\n");
for (const peer of configuredPeers) {
console.log(` • ${peer.name}`);
}
}
if (errors.length > 0) {
console.log(
`\n(${errors.length} endpoints had errors or are not available)`,
);
}
// Export as JSON for programmatic use
console.log("\n--- JSON Output ---");
console.log(JSON.stringify(configuredPeers, null, 2));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
To continue your FAssets development journey, you can:
- Learn how to mint FXRP
- Understand how to redeem FXRP
- Explore FAssets system settings