Skip to main content

Mint FAssets

Overview

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

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.

See the Minting overview for more details.

Prerequisites

Minting Process Steps

The minting process is a multi-step process that involves the following steps:

  1. Reserve collateral from a suitable agent.
  2. Send the underlying asset (e.g., XRP) to the agent.
  3. Use Flare Data Connector (FDC) to generate proof of payment.
  4. Execute minting with the proof 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";

// 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";

// Use zero address for executor since we're not using it
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// 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() {
// Initialize the FAssets FXRP AssetManager contract
const AssetManager = artifacts.require("IAssetManager");
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 IAssetManager = artifacts.require("IAssetManager");
const iAssetManager: IAssetManagerInstance = await IAssetManager.at(
ASSET_MANAGER_ADDRESS,
);

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

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

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

// 10. 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 FAsset lots to reserve.
  • UNDERLYING_ADDRESS: Target XRP Ledger address for the minted asset.
  • ZERO_ADDRESS: Placeholder for executor (not used in this script).
  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 findBestAgent() 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. Reserve collateral from agent by calling reserveCollateral()
  8. Call assetMintingDecimals() to determine the XRP token's decimal precision.
  9. Parse the CollateralReserved event.
  10. 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 wallet.
  5. Construct the payment transaction.
  6. Sign and submit the transaction.

Generate Proof with Flare Data Connector

Use the FDC Payment script to validate the XRP payment and generate a Merkle proof.

Execute Minting

Once the XRP payment is validated, you can retrieve the FDC proof from the Data Availability Layer and call the executeMinting() function on the AssetManager contract.

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, 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.