Skip to main content

Mint FAssets using the Executor

Overview

This guide walks you through the complete process of minting FAssets (e.g., FXRP) on the Flare network using the executor.

Minting FAssets is the process of wrapping, for instance, XRP from the XRP Ledger into an FAsset, enabling it to be used within the Flare blockchain ecosystem.

info

Executors are external actors that monitor pending minting requests and execute them by submitting payment proofs on-chain. They are incentivized for this task but have no special permissions—anyone can act as an executor. If no one executes in time, the minting request expires, and the process must be restarted.

An executor can be a wallet or a DApp platform that facilitates minting and helps users automate the flow.

See the Minting overview for more details.

Prerequisites

Minting Process Steps

The minting process involves the following steps:

  1. Reserve collateral from a suitable agent and nominate an executor.
  2. Send the underlying asset (e.g., XRP) to the agent.
  3. Use Flare Data Connector (FDC) to generate a proof of payment.
  4. Execute minting with the proof as a parameter to receive FAssets.

Reserve Collateral

The following code demonstrates how to reserve collateral by calling the reserveCollateral function on the AssetManager contract.

scripts/fassets/reserveCollateral.ts
import { ethers } from "hardhat";

import {
IAssetManagerInstance,
IAssetManagerContract,
} from "../../typechain-types";

// Initialize the FAssets FXRP AssetManager contract
const AssetManager = artifacts.require("IAssetManager");

// 1. Define constants

// AssetManager address on Flare Testnet Coston2 network
const ASSET_MANAGER_ADDRESS = "0xDeD50DA9C3492Bee44560a4B35cFe0e778F41eC5";
// Number of lots to reserve
const LOTS_TO_MINT = 1;
// XRP Ledger address
const UNDERLYING_ADDRESS = "rSHYuiEvsYsKR8uUHhBTuGP5zjRcGt4nm";

// Executor address
const EXECUTOR_ADDRESS = "0xb292348a4Cb9f5F008589B3596405FBba6986c55";

// 2. Function to find the best agent with enough free collateral lots

// Function from FAssets Bot repository
// https://github.com/flare-foundation/fasset-bots/blob/main/packages/fasset-bots-core/src/commands/InfoBotCommands.ts#L83
async function findBestAgent(
assetManager: IAssetManagerInstance,
minAvailableLots = 1,
) {
// get max 100 agents
const agents = (await assetManager.getAvailableAgentsDetailedList(0, 100))
._agents;

// filter agents with enough free collateral lots
let agentsWithLots = agents.filter(
(agent) => agent.freeCollateralLots > minAvailableLots,
);

if (agentsWithLots.length === 0) {
return undefined;
}

// sort by lowest fee
agentsWithLots.sort((a, b) => a.feeBIPS - b.feeBIPS);

while (agentsWithLots.length > 0) {
const lowestFee = agentsWithLots[0].feeBIPS;

// get all agents with the lowest fee
let optimal = agentsWithLots.filter((a) => a.feeBIPS == lowestFee);

while (optimal.length > 0) {
// const agentVault = (requireNotNull(randomChoice(optimal)) as any).agentVault; // list must be nonempty

// get a random agent from the list
const agentVault =
optimal[Math.floor(Math.random() * optimal.length)].agentVault;
// const agentVault = (randomChoice(optimal) as any).agentVault;

const info = await assetManager.getAgentInfo(agentVault);
// 0 = NORMAL
if (Number(info.status) === 0) {
return agentVault;
}
// If not found remote this agent and do another round
optimal = optimal.filter((a) => a.agentVault !== agentVault);
agentsWithLots = agentsWithLots.filter(
(a) => a.agentVault !== agentVault,
);
}
}
}

// 3. Function to parse the CollateralReserved event

async function parseCollateralReservedEvent(transactionReceipt) {
console.log("\nParsing events...", transactionReceipt.rawLogs);

const assetManager = (await ethers.getContractAt(
"IAssetManager",
ASSET_MANAGER_ADDRESS,
)) as IAssetManagerContract;

for (const log of transactionReceipt.rawLogs) {
try {
const parsedLog = assetManager.interface.parseLog({
topics: log.topics,
data: log.data,
});

if (!parsedLog) continue;

const collateralReservedEvents = ["CollateralReserved"];
if (!collateralReservedEvents.includes(parsedLog.name)) continue;

console.log(`\nEvent: ${parsedLog.name}`);
console.log("Arguments:", parsedLog.args);
const collateralReservedEvent = parsedLog.args;

return collateralReservedEvent;
} catch (e) {
console.log("Error parsing event:", e);
}
}
}

// 4. Main function

async function main() {
const assetManager: IAssetManagerInstance = await AssetManager.at(
ASSET_MANAGER_ADDRESS,
);

// 5. Find the best agent with enough free collateral lots
const agentVaultAddress = await findBestAgent(assetManager, LOTS_TO_MINT);
if (!agentVaultAddress) {
throw new Error("No suitable agent found with enough free collateral lots");
}
console.log(agentVaultAddress);

// 6. Get the agent info
const agentInfo = await assetManager.getAgentInfo(agentVaultAddress);
console.log("Agent info:", agentInfo);

// 7. Get the collateral reservation fee according to the number of lots to reserve
// https://dev.flare.network/fassets/minting/#collateral-reservation-fee
const collateralReservationFee =
await assetManager.collateralReservationFee(LOTS_TO_MINT);
console.log(
"Collateral reservation fee:",
collateralReservationFee.toString(),
);
const assetManager: IAssetManagerInstance = await AssetManager.at(
ASSET_MANAGER_ADDRESS,
);

// 8. To make this example simpler we're using the same fee for the executor and the agent
const executorFee = collateralReservationFee;
const totalFee = collateralReservationFee.add(executorFee);

console.log("Total reservation fee:", totalFee.toString());

// 9. Reserve collateral
// https://dev.flare.network/fassets/reference/IAssetManager#reservecollateral
const tx = await assetManager.reserveCollateral(
agentVaultAddress,
LOTS_TO_MINT,
agentInfo.feeBIPS,
// Not using the executor
EXECUTOR_ADDRESS,
[UNDERLYING_ADDRESS],
// Sending the collateral reservation fee as native tokens
{ value: totalFee },
);

console.log("Collateral reservation successful:", tx);

// 10. Get the asset decimals
const decimals = await assetManager.assetMintingDecimals();

// 11. Parse the CollateralReserved event
const collateralReservedEvent = await parseCollateralReservedEvent(
tx.receipt,
);

const collateralReservationInfo =
await assetManager.collateralReservationInfo(
collateralReservedEvent.collateralReservationId,
);
console.log("Collateral reservation info:", collateralReservationInfo);

// 11. Calculate the total amount of XRP to pay
const totalUBA =
collateralReservedEvent.valueUBA + collateralReservedEvent.feeUBA;
const totalXRP = Number(totalUBA) / 10 ** decimals;
console.log(`You need to pay ${totalXRP} XRP`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Collateral Reservation Script Breakdown

  1. Define constants
  • ASSET_MANAGER_ADDRESS: FXRP AssetManager address on Songbird Testnet (Coston).
  • LOTS_TO_MINT: Number of FAssets lots to reserve.
  • UNDERLYING_ADDRESS: Target XRP Ledger address for the minted asset.
  • EXECUTOR_ADDRESS: Executor address that will execute the minting process and provide the proof of underlying asset payment.
  1. Retrieve and filter agents with enough free collateral and select the agent with the lowest fee and normal status.
  2. Parse CollateralReserved event.
  3. Start the minting reservation process at the script's entry point.
  4. Call findBestAgen with the required number of lots.
  5. Fetch agent metadata from getAgentInfo to get the agent's feeBIPS, which is used to calculate the collateral reservation fee.
  6. Calculate the collateral reservation fee by calling collateralReservationFee.
  7. Set the executor fee the same as the collateral reservation fee to make this example simpler.
  8. Reserve collateral from agent by calling reserveCollateral with the executor address. The executor fee is on top of the collateral reservation fee in native tokens. The fee is agreed between the minter and the executor.
  9. Call assetMintingDecimals to determine the XRP token's decimal precision.
  10. Parse the CollateralReserved event to get the collateral reservation ID and other important collateral reservation details.
  11. Calculate the total XRP value required for payment.

Send Payment on XRP Ledger

The next step is to send the XRP Ledger payment to the agent, and you can use this script to do that.

scripts/fassets/xrpPayment.ts
// 1. install xrpl package
// https://www.npmjs.com/package/xrpl
import { Client, Wallet, xrpToDrops, Payment, TxResponse } from "xrpl";

// 2. Define the constants
const AGENT_ADDRESS = "r4KgCNzn9ZuNjpf17DEHZnyyiqpuj599Wm"; // Agent underlying chain address
const AMOUNT_XRP = "10.025"; // XRP amount to send
const PAYMENT_REFERENCE =
"4642505266410001000000000000000000000000000000000000000000f655fb"; // Reference

async function send20XrpWithReference() {
// 3. Create a client to connect to the XRP Ledger Testnet
const client = new Client("wss://s.altnet.rippletest.net:51233"); // Testnet
await client.connect();

// 4. XRP Ledger Testnet seed
const wallet: Wallet = Wallet.fromSeed("s000000000000000000000000000000"); // Sender wallet seed

// 5. Create a payment transaction
const paymentTx: Payment = {
TransactionType: "Payment",
Account: wallet.classicAddress,
// Agent underlying chain address
Destination: AGENT_ADDRESS,
// XRP amount to send
Amount: xrpToDrops(AMOUNT_XRP),
// Payment reference
Memos: [
{
Memo: {
MemoData: PAYMENT_REFERENCE,
},
},
],
};

console.log(paymentTx);

// 6. Execute the transaction
const prepared = await client.autofill(paymentTx);
const signed = wallet.sign(prepared);
const result: TxResponse = await client.submitAndWait(signed.tx_blob);

console.log("Transaction hash:", signed.hash);
console.log("Explorer: https://testnet.xrpl.org/transactions/" + signed.hash);
console.log("Result:", result);

await client.disconnect();
}

send20XrpWithReference().catch(console.error);

XRP Payment Script Breakdown

  1. Install the xrpl package — it's not included in the Flare Hardhat Starter Kit by default.
  2. Specify the correct constants from the reserve collateral script:
    • AGENT_ADDRESS - Agent's XRP Ledger address.
    • AMOUNT_XRP - XRP amount to send.
    • PAYMENT_REFERENCE - Payment reference from the the reserve collateral script.
  3. Create a client to connect to the XRP Ledger Testnet.
  4. Load the sender's wallet.
  5. Construct the payment transaction.
  6. Sign and submit the transaction.

Generate Proof with Flare Data Connector

The executor will use the Flare Data Connector's Payment attestastion type to validate the XRP payment and generate a Merkle proof.

Execute Minting

Once the XRP payment is validated, executor can retrieve the FDC proof from the Data Availability Layer and call the executeMinting function on the AssetManager contract. Once the minting is executed, the executor will receive the executor fee in native tokens. If the executor fails to execute in time, the request expires, and minting must be restarted.

This script demonstrates how to retrieve the FDC proof and execute minting.

scripts/fassets/executeMinting.ts
import { artifacts, ethers } from "hardhat";

import { prepareAttestationRequestBase } from "../fdcExample/Base";
import {
IAssetManagerInstance,
IAssetManagerContract,
} from "../../typechain-types";

// 1. Environment variables
const { COSTON2_DA_LAYER_URL, VERIFIER_URL_TESTNET, VERIFIER_API_KEY_TESTNET } =
process.env;

// 2. AssetManager address on Flare Testnet Coston2 network
const ASSET_MANAGER_ADDRESS = "0xDeD50DA9C3492Bee44560a4B35cFe0e778F41eC5";

// 3. Collateral reservation ID
const COLLATERAL_RESERVATION_ID = 18615047;

// 4. Data to get the proof for
const TARGET_ROUND_ID = 987510;

const attestationTypeBase = "Payment";
const sourceIdBase = "testXRP";
const verifierUrlBase = VERIFIER_URL_TESTNET;
const urlTypeBase = "xrp";

// XRP transaction
const transactionId =
"65520665BB83D582E01D6813DA8B5ECB041F613F9891F9BE90EE2668AAC30543";
const inUtxo = "0";
const utxo = "0";

// 5. Prepare FDC request
async function prepareFdcRequest(
transactionId: string,
inUtxo: string,
utxo: string,
) {
const requestBody = {
transactionId: transactionId,
inUtxo: inUtxo,
utxo: utxo,
};

const url = `${verifierUrlBase}verifier/${urlTypeBase}/Payment/prepareRequest`;

return await prepareAttestationRequestBase(
url,
VERIFIER_API_KEY_TESTNET,
attestationTypeBase,
sourceIdBase,
requestBody,
);
}

// 6. Get proof from FDC
async function getProof(roundId: number) {
const request = await prepareFdcRequest(transactionId, inUtxo, utxo);
const proofAndData = await fetch(
`${COSTON2_DA_LAYER_URL}api/v0/fdc/get-proof-round-id-bytes`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": VERIFIER_API_KEY_TESTNET,
},
body: JSON.stringify({
votingRoundId: roundId,
requestBytes: request.abiEncodedRequest,
}),
},
);

return await proofAndData.json();
}

async function parseEvents(receipt) {
console.log("\nParsing events...", receipt.rawLogs);

const assetManager = (await ethers.getContractAt(
"IAssetManager",
ASSET_MANAGER_ADDRESS,
)) as IAssetManagerContract;

for (const log of receipt.rawLogs) {
try {
const parsedLog = assetManager.interface.parseLog({
topics: log.topics,
data: log.data,
});

if (!parsedLog) continue;

const collateralReservedEvents = [
"RedemptionTicketCreated",
"MintingExecuted",
];
if (!collateralReservedEvents.includes(parsedLog.name)) continue;

console.log(`\nEvent: ${parsedLog.name}`);
console.log("Arguments:", parsedLog.args);
} catch (e) {
console.log("Error parsing event:", e);
}
}
}

async function main() {
// 7. Get proof from FDC
const proof = await getProof(TARGET_ROUND_ID);

// FAssets FXRP asset manager on Songbird Testnet Coston2 network
const AssetManager = artifacts.require("IAssetManager");
const assetManager: IAssetManagerInstance = await AssetManager.at(
ASSET_MANAGER_ADDRESS,
);

// 8. Execute minting with the proof
const tx = await assetManager.executeMinting(
{
merkleProof: proof.proof,
data: proof.response,
},
COLLATERAL_RESERVATION_ID,
);
console.log("Transaction successful:", tx);

// 9. Parse execute minting log events
await parseEvents(tx.receipt);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Execute Minting Script Breakdown

  1. Get environment variables.

  2. Declare the constant ASSET_MANAGER_ADDRESS pointing to the FXRP AssetManager on the Songbird Testnet (Coston network).

  3. Set the collateral reservation ID to the previously reserved minting request.

  4. Set the Flare Data Connector (FDC) round ID to retrieve the proof.

  5. Prepare the FDC request payload data.

  6. Create a function to get the proof from the FDC. It sends a POST request to the Flare Data Availability Layer and returns a Merkle proof and attestation response from FDC.

  7. Retrieve the FDC proof from the Data Availability Layer.

  8. Call the executeMinting function on the AssetManager contract and send a transaction to the Flare network to convert the attested XRP payment into FXRP (minting).

  9. On a successful transaction call parseExecutemintingEvents to extract and log events RedemptionTicketCreated and MintingExecuted.

Next Steps

Now that you have successfully minted FAssets using the executor, you can use them in Flare dApps or transfer them to other users or smart contracts within the Flare ecosystem.

Redeem

To convert your FAssets (e.g., FXRP) back into the original asset (e.g., XRP), follow the steps in the FAssets Redemption Guide.