Transfer frxUSD with Fraxtal
This guide demonstrates how to use the viem framework in a simple script that enables a user to transfer frxUSD with Fraxtal as the destination and source chain using the Lockbox addresses listed on the FraxZero Lockbox addresses.
In this example, we will be transferring frxUSD from Ethereum mainnet to Fraxtal mainnet, Fraxtal mainnet to Ethereum mainnet, and Arbitrum mainnet to Fraxtal mainnet.
Note: This guide does not support frxUSD transfers where Fraxtal is neither the source nor destination chain.
Prerequisites
Before you start building the sample app to perform a frxUSD transfer, ensure you have met the following prerequisites:
-
Install Node.js and npm
- Download and install Node.js directly or use a version manager like nvm.
- npm is included with Node.js.
-
Set up a non-custodial wallet (for example, MetaMask)
- You can download, install, and create a MetaMask wallet from its official website.
- During setup, create a wallet on Ethereum mainnet.
- Retrieve the private key for your wallet, as it will be required in the script below.
-
Fund your wallet with the gas tokens on the source chains
- For this guide, we will be transferring frxUSD from Ethereum mainnet, Arbitrum mainnet, and Fraxtal mainnet. Therefore, you will need to fund your wallet with ETH on Ethereum mainnet and Arbitrum mainnet, and FRAX on Fraxtal mainnet.
-
Fund your wallet with frxUSD on the source chains
- For this guide, we will be transferring frxUSD from Ethereum mainnet, Arbitrum mainnet, and Fraxtal mainnet. Therefore, you will need to fund your wallet (opens in a new tab) with frxUSD on each network.
Project setup
To build the script, first set up your project environment and install the required dependencies.
1. Set up a new project
Create a new directory and initialize a new Node.js
project with default settings:
mkdir frxusd-cross-chain-transfers-with-fraxtal
cd frxusd-cross-chain-transfers-with-fraxtal
npm init -y
This also creates a default package.json
file.
2. Install dependencies
In your project directory, install the required dependencies, including viem
:
npm install dotenv@^16.4.7 viem@^2.23.4
This sets up your development environment with the necessary libraries for building the script. It also updates the package.json
file with the dependencies.
3. Add module type
Add "type": "module"
to the package.json
file:
{
"name": "frax-cross-chain-transfer",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node transfer.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"dotenv": "^16.4.7",
"viem": "^2.23.4"
}
}
4. Configure environment variables
Create a .env
file in your project directory and add your wallet private key:
echo "PRIVATE_KEY=your-private-key-here" > .env
Warning: This is strictly for testing purposes. Never share your private key.
Script setup
This section covers the necessary setup for the transfer.js script, including defining keys and addresses, and configuring the wallet client for interacting with the source and destination chains.
1. Replace with your private key and wallet address
Ensure that this section of the file includes your private key for PRIVATE_KEY
and associated wallet address for DESTINATION_ADDRESS
.
The script also predefines the Lockbox contract addresses, the frxUSD contract addresses, the destination EIDs, and the transfer amount. These definitions are critical for successfully transferring frxUSD between the intended wallets.
// ============ Configuration Constants ============
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
// Recipient address
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Enter your recipient wallet address here
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
2,
)}`; // Convert to bytes32 format
// LockBox Contract Addresses and Destination EID
const ETHEREUM_MAINNET_LOCKBOX_ADDRESS = '0x566a6442a5a6e9895b9dca97cc7879d632c6e4b0'; // Lockbox contract address (used for Ethereum -> Fraxtal transfers)
const FRAXTAL_MAINNET_LOCKBOX_ADDRESS = '0x96A394058E2b84A89bac9667B19661Ed003cF5D4'; // Lockbox contract address (used for Fraxtal -> Ethereum transfers)
// frxUSD token contract addresses
const ARBITRUM_MAINNET_FRXUSD_ADDRESS = '0x80Eede496655FB9047dd39d9f418d5483ED600df'; // frxUSD token contract address (used for Arbitrum -> Fraxtal transfers)
const FRAXTAL_MAINNET_FRXUSD_ADDRESS = '0xfc00000000000000000000000000000000000001'; // frxUSD token contract address on Fraxtal mainnet
const ETHEREUM_MAINNET_FRXUSD_ADDRESS = '0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29'; // frxUSD contract address on Ethereum mainnet
// EIDs for destination chains
const FRAXTAL_MAINNET_EID = 30255; // EID of Fraxtal mainnet
const ETHEREUM_MAINNET_EID = 30101; // EID of Ethereum mainnet
// Transfer Parameters
const TRANSFER_AMOUNT = 1_000_000_000_000_000_000n; // 1 frxUSD (18 decimals)
2. Set up wallet and public clients
The wallet clients and public clients configure the appropriate network settings using viem
. In this example guide, the script will be conducting the following frxUSD transfers:
- Ethereum mainnet to Fraxtal mainnet
- Fraxtal mainnet to Ethereum mainnet
- Arbitrum mainnet to Fraxtal mainnet
// Set up wallet clients
const ethereumWalletClient = createWalletClient({
chain: mainnet,
transport: http(),
account,
});
const fraxtalWalletClient = createWalletClient({
chain: fraxtal,
transport: http(),
account,
});
const arbitrumWalletClient = createWalletClient({
chain: arbitrum,
transport: http(),
account,
});
// Set up public clients for different chains
const ethereumPublicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const fraxtalPublicClient = createPublicClient({
chain: fraxtal,
transport: http(),
});
const arbitrumPublicClient = createPublicClient({
chain: arbitrum,
transport: http(),
});
frxUSD transfer process with Fraxtal
The following sections outline the relevant transfer logic of the sample script. In this example, we are conducting the following frxUSD transfers:
- 1 frxUSD from Ethereum mainnet to Fraxtal mainnet
- 1 frxUSD from Fraxtal mainnet to Ethereum mainnet
- 1 frxUSD from Arbitrum mainnet to Fraxtal mainnet
1. Approve frxUSD
The first step is to grant approval for the lockbox address. To determine the lockbox address to approve for each transfer, you can use the following table:
Source Chain | Destination Chain | Lockbox Address |
---|---|---|
Ethereum | Fraxtal | ETHEREUM_MAINNET_LOCKBOX_ADDRESS |
Fraxtal | Ethereum (or other EVM chains) | FRAXTAL_MAINNET_LOCKBOX_ADDRESS |
Arbitrum (or other EVM chains excluding Fraxtal and Ethereum) | Fraxtal | NO APPROVAL REQUIRED |
Note: If the source chain is Arbitrum mainnet (or other EVM chains excluding Fraxtal and Ethereum) and the destination chain is Fraxtal mainnet, you do not need to approve the frxUSD token. However, if the source chain is Fraxtal mainnet and the destination chain is Arbitrum mainnet (or other EVM chains), you do need to approve the frxUSD token.
async function approveFrxUSD(sourceChainClient, frxUsdTokenAddress, lockboxAddress) {
console.log('Approving frxUSD transfer...');
const approveTx = await sourceChainClient.sendTransaction({
to: frxUsdTokenAddress,
data: encodeFunctionData({
abi: [
{
type: 'function',
name: 'approve',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
],
functionName: 'approve',
args: [lockboxAddress, TRANSFER_AMOUNT], // Set max allowance in wei (change as needed)
}),
});
console.log(`frxUSD Approval Tx: ${approveTx}`);
}
async function approveFrxUSDForEthereumToFraxtalTransfer() {
await approveFrxUSD(
ethereumWalletClient,
ETHEREUM_MAINNET_FRXUSD_ADDRESS,
ETHEREUM_MAINNET_LOCKBOX_ADDRESS,
);
}
async function approveFrxUSDForFraxtalToEthereumTransfer() {
await approveFrxUSD(
fraxtalWalletClient,
FRAXTAL_MAINNET_FRXUSD_ADDRESS,
FRAXTAL_MAINNET_LOCKBOX_ADDRESS,
);
}
2. Retrieve FraxZero Quote
In this step, you call the quoteSend
function on the following depending on the transfer direction:
Source Chain | Destination Chain | Address to call quoteSend on |
---|---|---|
Ethereum | Fraxtal | ETHEREUM_MAINNET_LOCKBOX_ADDRESS |
Fraxtal | Ethereum (or other EVM chains) | FRAXTAL_MAINNET_LOCKBOX_ADDRESS |
Arbitrum (or other EVM chains excluding Fraxtal and Ethereum) | Fraxtal | ARBITRUM_MAINNET_FRXUSD_ADDRESS |
Note: If the source chain is Arbitrum mainnet (or other EVM chains excluding Fraxtal and Ethereum) and the destination chain is Fraxtal mainnet, you simply use the frxUSD
token address.
You then specify the following parameters:
- Destination EID: The EID of the destination chain (EIDs can be found here)
- Destination address: The wallet address that will receive the transferred frxUSD (in bytes32 format)
- Amount: The amount of frxUSD to transfer (in wei)
- Minimum amount: The minimum amount of frxUSD to receive (in wei)
- Extra options: The extra options for the transfer
- Compose message: The compose message for the transfer
- OFT command: The OFT command for the transfer
The quoteSend
function returns the native fee amount required for the transfer.
async function retrieveQuote(
sourceChainClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
) {
console.log('Retrieving quote...');
const quote = await sourceChainClient.readContract({
address: lockboxAddressOrFrxUsdTokenAddress,
abi: [
{
inputs: [
{
components: [
{ internalType: 'uint32', name: 'dstEid', type: 'uint32' },
{ internalType: 'bytes32', name: 'to', type: 'bytes32' },
{ internalType: 'uint256', name: 'amountLD', type: 'uint256' },
{
internalType: 'uint256',
name: 'minAmountLD',
type: 'uint256',
},
{ internalType: 'bytes', name: 'extraOptions', type: 'bytes' },
{ internalType: 'bytes', name: 'composeMsg', type: 'bytes' },
{ internalType: 'bytes', name: 'oftCmd', type: 'bytes' },
],
internalType: 'struct SendParam',
name: '_sendParam',
type: 'tuple',
},
{ internalType: 'bool', name: '_payInLzToken', type: 'bool' },
],
name: 'quoteSend',
outputs: [
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{
internalType: 'uint256',
name: 'lzTokenFee',
type: 'uint256',
},
],
internalType: 'struct MessagingFee',
name: 'msgFee',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
],
functionName: 'quoteSend',
args: [
{
dstEid: destinationEid,
to: DESTINATION_ADDRESS_BYTES32,
amountLD: TRANSFER_AMOUNT,
minAmountLD: (TRANSFER_AMOUNT * 99n) / 100n,
extraOptions: '0x',
composeMsg: '0x',
oftCmd: '0x',
},
false,
],
});
const nativeFee = quote.nativeFee;
return nativeFee;
}
3. Transfer frxUSD
After retrieving the native fee amounts, you can call the send
function on the address you used in the prior step:
Source Chain | Destination Chain | Address to call send on |
---|---|---|
Ethereum | Fraxtal | ETHEREUM_MAINNET_LOCKBOX_ADDRESS |
Fraxtal | Ethereum (or other EVM chains) | FRAXTAL_MAINNET_LOCKBOX_ADDRESS |
Arbitrum (or other EVM chains excluding Fraxtal and Ethereum) | Fraxtal | ARBITRUM_MAINNET_FRXUSD_ADDRESS |
Note: If the source chain is Arbitrum mainnet (or other EVM chains excluding Fraxtal and Ethereum) and the destination chain is Fraxtal mainnet, you simply use the frxUSD
token address.
This function takes the following parameters:
- Destination EID: The EID of the destination chain
- Destination address: The wallet address that will receive the transferred frxUSD (in bytes32 format)
- Amount: The amount of frxUSD to transfer (in wei)
- Minimum amount: The minimum amount of frxUSD to receive (in wei)
- Extra options: The extra options for the transfer
- Compose message: The compose message for the transfer
- OFT command: The OFT command for the transfer
- Native fee: The native fee for the transfer
- Lz token fee: The lz token fee for the transfer
- Refund address: The refund address for the transfer
async function transferFrxUSD(
sourceWalletClient,
sourcePublicClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
) {
console.log('Transferring frxUSD...');
const nativeFee = await retrieveQuote(
sourcePublicClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
);
const transferTx = await sourceWalletClient.sendTransaction({
to: lockboxAddressOrFrxUsdTokenAddress,
value: nativeFee,
data: encodeFunctionData({
abi: [
{
inputs: [
{
components: [
{ internalType: 'uint32', name: 'dstEid', type: 'uint32' },
{ internalType: 'bytes32', name: 'to', type: 'bytes32' },
{ internalType: 'uint256', name: 'amountLD', type: 'uint256' },
{ internalType: 'uint256', name: 'minAmountLD', type: 'uint256' },
{ internalType: 'bytes', name: 'extraOptions', type: 'bytes' },
{ internalType: 'bytes', name: 'composeMsg', type: 'bytes' },
{ internalType: 'bytes', name: 'oftCmd', type: 'bytes' },
],
internalType: 'struct SendParam',
name: '_sendParam',
type: 'tuple',
},
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{ internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
],
internalType: 'struct MessagingFee',
name: '_fee',
type: 'tuple',
},
{ internalType: 'address', name: '_refundAddress', type: 'address' },
],
name: 'send',
outputs: [
{
components: [
{ internalType: 'bytes32', name: 'guid', type: 'bytes32' },
{ internalType: 'uint64', name: 'nonce', type: 'uint64' },
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{ internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
],
internalType: 'struct MessagingFee',
name: 'fee',
type: 'tuple',
},
],
internalType: 'struct MessagingReceipt',
name: 'msgReceipt',
type: 'tuple',
},
{
components: [
{ internalType: 'uint256', name: 'amountSentLD', type: 'uint256' },
{
internalType: 'uint256',
name: 'amountReceivedLD',
type: 'uint256',
},
],
internalType: 'struct OFTReceipt',
name: 'oftReceipt',
type: 'tuple',
},
],
stateMutability: 'payable',
type: 'function',
},
],
functionName: 'send',
args: [
{
dstEid: destinationEid,
to: DESTINATION_ADDRESS_BYTES32,
amountLD: TRANSFER_AMOUNT,
minAmountLD: (TRANSFER_AMOUNT * 99n) / 100n,
extraOptions: '0x',
composeMsg: '0x',
oftCmd: '0x',
},
{ nativeFee: nativeFee, lzTokenFee: 0n },
account.address,
],
value: nativeFee,
}),
});
console.log();
console.log(`Transfer Tx: ${transferTx}`);
console.log(`View on LayerZero explorer: https://layerzeroscan.com/tx/${transferTx}`);
}
// Here, because Ethereum mainnet is the source chain for this transfer to Fraxtal, we use the lockbox address instead of the frxUSD token address.
async function transferFrxUSDForEthereumToFraxtal() {
await transferFrxUSD(
ethereumWalletClient,
ethereumPublicClient,
ETHEREUM_MAINNET_LOCKBOX_ADDRESS,
FRAXTAL_MAINNET_EID,
);
}
// Here, because Fraxtal mainnet is the source chain for this transfer, we use the lockbox address instead of the frxUSD token address.
async function transferFrxUSDForFraxtalToEthereum() {
await transferFrxUSD(
fraxtalWalletClient,
fraxtalPublicClient,
FRAXTAL_MAINNET_LOCKBOX_ADDRESS,
ETHEREUM_MAINNET_EID,
);
}
// Here, because Arbitrum mainnet is the source chain for this transfer, we use the frxUSD token address instead of the lockbox address.
async function transferFrxUSDForArbitrumToFraxtal() {
await transferFrxUSD(
arbitrumWalletClient,
arbitrumPublicClient,
ARBITRUM_MAINNET_FRXUSD_ADDRESS,
FRAXTAL_MAINNET_EID,
);
}
Build the script
Now that you understand the core steps for programmatically transferring frxUSD with Fraxtal as the source or destination chain using Frax's cross-chain infrastructure, create a transfer.js
in your project directory and populate it with the sample code below.
Note: The source wallet must contain native gas tokens (to cover gas fees) and frxUSD to complete the transfer.
transfer.js
// Import environment variables
import 'dotenv/config';
import {
createWalletClient,
createPublicClient,
http,
encodeFunctionData,
nonceManager,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet, arbitrum, fraxtal } from 'viem/chains';
// ============ Configuration Constants ============
// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`, { nonceManager });
// Recipient address
const DESTINATION_ADDRESS = 'enter-your-recipient-wallet-address-here'; // Enter your recipient wallet address here
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(
2,
)}`; // Convert to bytes32 format
// LockBox Contract Addresses and Destination EID
const ETHEREUM_MAINNET_LOCKBOX_ADDRESS = '0x566a6442a5a6e9895b9dca97cc7879d632c6e4b0'; // Lockbox contract address (used for Ethereum -> Fraxtal transfers)
const FRAXTAL_MAINNET_LOCKBOX_ADDRESS = '0x96A394058E2b84A89bac9667B19661Ed003cF5D4'; // Lockbox contract address (used for Fraxtal -> Ethereum transfers)
// frxUSD token contract addresses
const ARBITRUM_MAINNET_FRXUSD_ADDRESS = '0x80Eede496655FB9047dd39d9f418d5483ED600df'; // frxUSD token contract address (used for Arbitrum -> Fraxtal transfers)
const FRAXTAL_MAINNET_FRXUSD_ADDRESS = '0xfc00000000000000000000000000000000000001'; // frxUSD token contract address on Fraxtal mainnet
const ETHEREUM_MAINNET_FRXUSD_ADDRESS = '0xCAcd6fd266aF91b8AeD52aCCc382b4e165586E29'; // frxUSD contract address on Ethereum mainnet
// EIDs for destination chains
const FRAXTAL_MAINNET_EID = 30255; // EID of Fraxtal mainnet
const ETHEREUM_MAINNET_EID = 30101; // EID of Ethereum mainnet
// Transfer Parameters
const TRANSFER_AMOUNT = 10_000_000_000_000_000n; // 1 frxUSD (18 decimals)
// Set up wallet clients
const ethereumWalletClient = createWalletClient({
chain: mainnet,
transport: http(),
account,
});
const fraxtalWalletClient = createWalletClient({
chain: fraxtal,
transport: http(),
account,
});
const arbitrumWalletClient = createWalletClient({
chain: arbitrum,
transport: http(),
account,
});
// Set up public clients for different chains
const ethereumPublicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const fraxtalPublicClient = createPublicClient({
chain: fraxtal,
transport: http(),
});
const arbitrumPublicClient = createPublicClient({
chain: arbitrum,
transport: http(),
});
async function approveFrxUSD(sourceChainClient, frxUsdTokenAddress, lockboxAddress) {
console.log('Approving frxUSD transfer...');
const approveTx = await sourceChainClient.sendTransaction({
to: frxUsdTokenAddress,
data: encodeFunctionData({
abi: [
{
type: 'function',
name: 'approve',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ name: '', type: 'bool' }],
},
],
functionName: 'approve',
args: [lockboxAddress, TRANSFER_AMOUNT], // Set max allowance in wei (change as needed)
}),
});
console.log(`frxUSD Approval Tx: ${approveTx}`);
}
async function approveFrxUSDForEthereumToFraxtalTransfer() {
await approveFrxUSD(
ethereumWalletClient,
ETHEREUM_MAINNET_FRXUSD_ADDRESS,
ETHEREUM_MAINNET_LOCKBOX_ADDRESS,
);
}
async function approveFrxUSDForFraxtalToEthereumTransfer() {
await approveFrxUSD(
fraxtalWalletClient,
FRAXTAL_MAINNET_FRXUSD_ADDRESS,
FRAXTAL_MAINNET_LOCKBOX_ADDRESS,
);
}
async function retrieveQuote(
sourceChainClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
) {
console.log('Retrieving quote...');
const quote = await sourceChainClient.readContract({
address: lockboxAddressOrFrxUsdTokenAddress,
abi: [
{
inputs: [
{
components: [
{ internalType: 'uint32', name: 'dstEid', type: 'uint32' },
{ internalType: 'bytes32', name: 'to', type: 'bytes32' },
{ internalType: 'uint256', name: 'amountLD', type: 'uint256' },
{
internalType: 'uint256',
name: 'minAmountLD',
type: 'uint256',
},
{ internalType: 'bytes', name: 'extraOptions', type: 'bytes' },
{ internalType: 'bytes', name: 'composeMsg', type: 'bytes' },
{ internalType: 'bytes', name: 'oftCmd', type: 'bytes' },
],
internalType: 'struct SendParam',
name: '_sendParam',
type: 'tuple',
},
{ internalType: 'bool', name: '_payInLzToken', type: 'bool' },
],
name: 'quoteSend',
outputs: [
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{
internalType: 'uint256',
name: 'lzTokenFee',
type: 'uint256',
},
],
internalType: 'struct MessagingFee',
name: 'msgFee',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
],
functionName: 'quoteSend',
args: [
{
dstEid: destinationEid,
to: DESTINATION_ADDRESS_BYTES32,
amountLD: TRANSFER_AMOUNT,
minAmountLD: (TRANSFER_AMOUNT * 99n) / 100n,
extraOptions: '0x',
composeMsg: '0x',
oftCmd: '0x',
},
false,
],
});
const nativeFee = quote.nativeFee;
return nativeFee;
}
async function transferFrxUSD(
sourceWalletClient,
sourcePublicClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
) {
console.log('Transferring frxUSD...');
const nativeFee = await retrieveQuote(
sourcePublicClient,
lockboxAddressOrFrxUsdTokenAddress,
destinationEid,
);
const transferTx = await sourceWalletClient.sendTransaction({
to: lockboxAddressOrFrxUsdTokenAddress,
value: nativeFee,
data: encodeFunctionData({
abi: [
{
inputs: [
{
components: [
{ internalType: 'uint32', name: 'dstEid', type: 'uint32' },
{ internalType: 'bytes32', name: 'to', type: 'bytes32' },
{ internalType: 'uint256', name: 'amountLD', type: 'uint256' },
{ internalType: 'uint256', name: 'minAmountLD', type: 'uint256' },
{ internalType: 'bytes', name: 'extraOptions', type: 'bytes' },
{ internalType: 'bytes', name: 'composeMsg', type: 'bytes' },
{ internalType: 'bytes', name: 'oftCmd', type: 'bytes' },
],
internalType: 'struct SendParam',
name: '_sendParam',
type: 'tuple',
},
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{ internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
],
internalType: 'struct MessagingFee',
name: '_fee',
type: 'tuple',
},
{ internalType: 'address', name: '_refundAddress', type: 'address' },
],
name: 'send',
outputs: [
{
components: [
{ internalType: 'bytes32', name: 'guid', type: 'bytes32' },
{ internalType: 'uint64', name: 'nonce', type: 'uint64' },
{
components: [
{ internalType: 'uint256', name: 'nativeFee', type: 'uint256' },
{ internalType: 'uint256', name: 'lzTokenFee', type: 'uint256' },
],
internalType: 'struct MessagingFee',
name: 'fee',
type: 'tuple',
},
],
internalType: 'struct MessagingReceipt',
name: 'msgReceipt',
type: 'tuple',
},
{
components: [
{ internalType: 'uint256', name: 'amountSentLD', type: 'uint256' },
{
internalType: 'uint256',
name: 'amountReceivedLD',
type: 'uint256',
},
],
internalType: 'struct OFTReceipt',
name: 'oftReceipt',
type: 'tuple',
},
],
stateMutability: 'payable',
type: 'function',
},
],
functionName: 'send',
args: [
{
dstEid: destinationEid,
to: DESTINATION_ADDRESS_BYTES32,
amountLD: TRANSFER_AMOUNT,
minAmountLD: (TRANSFER_AMOUNT * 99n) / 100n,
extraOptions: '0x',
composeMsg: '0x',
oftCmd: '0x',
},
{ nativeFee: nativeFee, lzTokenFee: 0n },
account.address,
],
value: nativeFee,
}),
});
console.log();
console.log(`Transfer Tx: ${transferTx}`);
console.log(`View on LayerZero explorer: https://layerzeroscan.com/tx/${transferTx}`);
}
// Here, because Ethereum mainnet is the source chain for this transfer to Fraxtal, we use the lockbox address instead of the frxUSD token address.
async function transferFrxUSDForEthereumToFraxtal() {
await transferFrxUSD(
ethereumWalletClient,
ethereumPublicClient,
ETHEREUM_MAINNET_LOCKBOX_ADDRESS,
FRAXTAL_MAINNET_EID,
);
}
// Here, because Fraxtal mainnet is the source chain for this transfer, we use the lockbox address instead of the frxUSD token address.
async function transferFrxUSDForFraxtalToEthereum() {
await transferFrxUSD(
fraxtalWalletClient,
fraxtalPublicClient,
FRAXTAL_MAINNET_LOCKBOX_ADDRESS,
ETHEREUM_MAINNET_EID,
);
}
// Here, because Arbitrum mainnet is the source chain for this transfer, we use the frxUSD token address instead of the lockbox address.
async function transferFrxUSDForArbitrumToFraxtal() {
await transferFrxUSD(
arbitrumWalletClient,
arbitrumPublicClient,
ARBITRUM_MAINNET_FRXUSD_ADDRESS,
FRAXTAL_MAINNET_EID,
);
}
async function main() {
if (DESTINATION_ADDRESS === 'enter-your-recipient-wallet-address-here') {
console.error(
'Destination address is not set. Please set the DESTINATION_ADDRESS variable in the transfer.js file.',
);
process.exit(1);
}
const command = process.argv[2]; // Get command from CLI
switch (command) {
case 'ethereum-to-fraxtal':
console.log('Running Ethereum → Fraxtal transfer...');
await approveFrxUSDForEthereumToFraxtalTransfer();
await transferFrxUSDForEthereumToFraxtal();
break;
case 'fraxtal-to-ethereum':
console.log('Running Fraxtal → Ethereum transfer...');
await approveFrxUSDForFraxtalToEthereumTransfer();
await transferFrxUSDForFraxtalToEthereum();
break;
case 'arbitrum-to-fraxtal':
console.log('Running Arbitrum → Fraxtal transfer...');
await approveFrxUSDForFraxtalToEthereumTransfer();
await transferFrxUSDForFraxtalToEthereum();
break;
default:
console.error(
'Invalid command. Please use "ethereum-to-arbitrum" or "arbitrum-to-ethereum".',
);
process.exit(1);
}
console.log('Transfer completed!');
}
main().catch(console.error);
The transfer.js
script provides a complete end-to-end solution for transferring frxUSD using Frax's cross-chain infrastructure with a non-custodial wallet. In the next section, you can test the script.
Test the script
To test the Ethereum to Fraxtal transfer, run the following command:
node transfer.js ethereum-to-fraxtal
To test the Fraxtal to Ethereum transfer, run the following command:
node transfer.js fraxtal-to-ethereum
To test the Arbitrum to Fraxtal transfer, run the following command:
node transfer.js arbitrum-to-fraxtal
Once each script runs and the transfer is finalized, a confirmation receipt and link on LayerZero scan (opens in a new tab) is logged in the console for each respective transfer. Each transfer takes approximately 4-5 minutes to complete.