Custom Instruction
The Custom Instruction overview introduces the 0xFE memo opcode, which keeps the XRPL memo at a fixed 42 bytes by carrying only keccak256(PackedUserOperation) and delivers the user operation bytes to Flare through an off-chain executor.
This guide walks through a TypeScript script that drives all three steps of the protocol with Viem and the Flare Data Connector (FDC).
A prerequisite for the user operation to be executed is that the personal account holds enough native tokens to cover the call values.
The script forwards a total of 2 C2FLR (Coston2 Flare native tokens), so fund the personal account from the Flare faucet before running it.
The State Lookup guide shows how to derive the personal account address.
The XRPL wallet that signs the Payment also needs enough XRP to cover the payment amount (net mint plus fees, computed below).
Before sending, the script reads the wallet's XRP balance with the getXrpBalance helper and aborts if it is below the required amount.
The full code is available on GitHub.
Contracts
The script executes three calls on three example smart contracts already deployed on Coston2. None of them is useful on its own; together they exercise the three things a custom instruction can do (call with no value, call with native value, call with a string argument and native value).
The first contract is a Checkpoint that counts how many times each user has called its passCheckpoint function.
The first call of our user operation will be to call the passCheckpoint function of this contract, deployed at the address 0xEE6D54382aA623f4D16e856193f5f8384E487002.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract Checkpoint {
mapping(address => uint256) public numberOfPasses;
function passCheckpoint() public payable {
++numberOfPasses[msg.sender];
}
}
The second contract is a PiggyBank, and is a degree more useful.
It allows a user to deposit FLR into it, and withdraw it all at once.
Our second call will be to deposit 1 FLR into the PiggyBank contract at the address 0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract PiggyBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0);
delete balances[msg.sender];
(bool success, ) = payable(msg.sender).call{ value: amount }("");
require(success);
}
}
The third and last contract is a NoticeBoard.
It allows users to pinNotice.
The value they send in this transaction will determine how long the notice stays up.
As the contract is set up, 1 FLR gets you 30 days, with fractions allowed.
The last call our personal account will perform is to pin a notice with the message Hello World! to the NoticeBoard at the address 0x59D57652BF4F6d97a6e555800b3920Bd775661Dc for 30 days (which means we attach a value of 1 FLR).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
struct Notice {
uint256 expirationTimestamp;
string message;
}
contract NoticeBoard {
uint256 public constant THIRTY_DAYS = 2592000;
mapping(address => Notice) public notices;
mapping(address => bool) public existingClients;
address[] public clients;
function pinNotice(string memory message) public payable {
require(msg.value > 0);
uint256 duration = THIRTY_DAYS * (msg.value / 1 ether);
uint256 expirationTimestamp = block.timestamp + duration;
notices[msg.sender] = Notice(expirationTimestamp, message);
if (!existingClients[msg.sender]) {
clients.push(msg.sender);
existingClients[msg.sender] = true;
}
}
function getNotices() public view returns (Notice[] memory) {
Notice[] memory _notices = new Notice[](clients.length);
for (uint256 i = 0; i < clients.length; ++i) {
Notice memory notice = notices[clients[i]];
if (notice.expirationTimestamp > block.timestamp) {
_notices[i] = notice;
}
}
return _notices;
}
}
The contract also performs additional checks to ensure each client is only added to the array once, and that getNotices returns only notices that have not yet expired.
Two Actors, Three Steps
The 0xFE flow is a three-step procedure that maps to two independent actors on mainnet.
This script runs all three steps from a single process for demo purposes, but the on-chain checks are designed around the split:
| Step | Actor | Action |
|---|---|---|
| 1 | User | Encode the PackedUserOperation as userOp, commit keccak256(userOp) in the 42-byte XRPL memo, send the XRPL Payment and send the userOp data to the Executor. |
| 2 | Executor | Fetch an FDC XRPPayment proof and call AssetManagerFXRP.executeDirectMintingWithData(proof, userOp). |
| 3 | Both | The MasterAccountController executes the user operation inside the executor's transaction and emits UserOperationExecuted. |
On mainnet, the user delivers the PackedUserOperation bytes to the executor off-chain (e.g. over an authenticated HTTP API); they never go onto the XRPL ledger.
For demo purposes this script plays both roles itself: the same process encodes the user operation, sends the XRPL Payment, then fetches the FDC proof and submits executeDirectMintingWithData.
In production, Step 2 will be run by an executor service - initially a service operated by Flare - and the user will only hand it (xrplTransactionHash, data, totalCallValue) over an authenticated channel.
The on-chain checks (XRPL signature, proofOwner binding, keccak256(data) commitment) are what make the split safe: the executor cannot substitute different bytes or run the user operation against a payment that was not theirs to deliver.
Building the Call Array
Each entry in the user operation is a Call struct with three fields:
target- the address of the Flare contract to invoke. The personal account dispatches the call with itself asmsg.sender, so this is the contract that ultimately runs the call.value- the amount of the native token (FLR on mainnet, C2FLR on Coston2) in wei to attach to the call. The sum ofvalueacross all calls is what the executor must attach asmsg.valueonexecuteDirectMintingWithDatain Step 2; that value flowsAssetManagerFXRP -> MasterAccountController -> PersonalAccount.executeUserOpand is split back across the inner calls.data- the ABI-encoded calldata (4-byte function selector followed by ABI-encoded arguments) the personal account passes totarget. Use Viem'sencodeFunctionDatato build it from the contract ABI, function name, and arguments.
export type Call = {
target: Address;
value: bigint;
data: `0x${string}`;
};
Because the 0xFE memo is independent of batch size, the script issues a single batch for all three calls - the same three that overflow the XRPL memo cap in the raw flow:
const calls: Call[] = [
{
target: checkpointAddress,
value: BigInt(0),
data: encodeFunctionData({
abi: checkpointAbi,
functionName: "passCheckpoint",
args: [],
}),
},
{
target: piggyBankAddress,
value: BigInt(depositAmount),
data: encodeFunctionData({
abi: piggyBankAbi,
functionName: "deposit",
args: [],
}),
},
{
target: noticeBoardAddress,
value: BigInt(pinNoticeAmount),
data: encodeFunctionData({
abi: noticeBoardAbi,
functionName: "pinNotice",
args: [pinNoticeMessage],
}),
},
];
Personal account and nonce
The personal account address is deterministic, so we can fetch it before the account is even deployed by calling getPersonalAccount on the MasterAccountController.
Before encoding the user operation, we also need the current nonce from getNonce.
Each successful execution increments it, so passing an invalid value reverts to InvalidNonce.
export async function getNonce(personalAccount: Address): Promise<bigint> {
return publicClient.readContract({
address: await getMasterAccountControllerAddress(),
abi: iMemoInstructionsFacetAbi,
functionName: "getNonce",
args: [personalAccount],
}) as Promise<bigint>;
}
Encoding the Hash Memo
The XRPL memo is a fixed 42 bytes: 10-byte header followed by the 32-byte hash of the ABI-encoded PackedUserOperation.
The ABI-encoded user operation bytes are returned alongside the memo so they can be handed to the executor:
export function encodeHashInstructionMemo({
calls,
walletId,
executorFeeUBA,
sender,
nonce,
}: {
calls: Call[];
walletId: number;
executorFeeUBA: bigint;
sender: Address;
nonce: bigint;
}): { memoData: `0x${string}`; data: `0x${string}` } {
const data = encodePackedUserOpData({ calls, sender, nonce });
const memoData = concatHex([
buildInstructionHeader("0xfe", walletId, executorFeeUBA),
keccak256(data),
]);
return { memoData, data };
}
The encodePackedUserOpData helper ABI-encodes a PackedUserOperation whose only meaningful fields are sender, nonce, and callData - the others are not validated on-chain.
The buildInstructionHeader helper packs the opcode (0xFE for the custom instruction, 0xFF for the raw variant), the one-byte walletId, and the 8-byte big-endian executorFeeUBA into the 10-byte instruction header.
Computing the XRPL Payment Amount
A custom instruction rides on top of an FAssets direct minting payment, so the XRPL transfer must cover the net mint amount plus the minting fee and the executor fee, as read from the AssetManagerFXRP contract.
For a memo-only transaction (no FXRP mint), the net amount is 0, but the fees still apply.
export async function computeDirectMintingPaymentAmountXrp({
netMintAmountXrp,
}: {
netMintAmountXrp: number;
}): Promise<number> {
const assetManagerAddress =
await getContractAddressByName("AssetManagerFXRP");
const [executorFeeUBA, feeBIPS, minimumFeeUBA] = await Promise.all([
publicClient.readContract({
address: assetManagerAddress,
abi: coston2.iDirectMintingSettingsAbi,
functionName: "getDirectMintingExecutorFeeUBA",
}),
publicClient.readContract({
address: assetManagerAddress,
abi: coston2.iDirectMintingSettingsAbi,
functionName: "getDirectMintingFeeBIPS",
}),
publicClient.readContract({
address: assetManagerAddress,
abi: coston2.iDirectMintingSettingsAbi,
functionName: "getDirectMintingMinimumFeeUBA",
}),
]);
const netMintUBA = BigInt(xrpToDrops(netMintAmountXrp));
const proportionalFeeUBA = (netMintUBA * feeBIPS) / 10_000n;
const mintingFeeUBA =
proportionalFeeUBA > minimumFeeUBA ? proportionalFeeUBA : minimumFeeUBA;
const totalUBA = netMintUBA + mintingFeeUBA + executorFeeUBA;
return Number(dropsToXrp(totalUBA.toString()));
}
The script also mints 10 FXRP as a side effect of the same payment, so the payment amount includes the net mint amount on top of the fees.
Checking the XRPL Wallet Balance
The signer of the XRPL Payment must hold at least paymentAmountXrp XRP, otherwise the payment will fail when submitted to the network.
The getXrpBalance helper issues an account_info request against the validated ledger and returns the wallet's balance in XRP.
It reuses the caller's Client when one is passed in, and otherwise opens and closes its own connection:
export async function getXrpBalance(
xrplAddress: string,
client?: Client,
): Promise<number> {
const ownsClient = client === undefined;
const xrplClient = client ?? new Client(process.env.XRPL_TESTNET_RPC_URL!);
if (!xrplClient.isConnected()) {
await xrplClient.connect();
}
try {
const response = await xrplClient.request({
command: "account_info",
account: xrplAddress,
ledger_index: "validated",
});
return Number(dropsToXrp(response.result.account_data.Balance));
} finally {
if (ownsClient) {
await xrplClient.disconnect();
}
}
}
The main function calls it right after computing paymentAmountXrp and aborts before touching XRPL if the wallet is short.
Failing fast here avoids a half-committed flow where the XRPL submission errors out after the user has already paid gas on Flare for unrelated reads:
const xrpBalance = await getXrpBalance(xrplWallet.address, xrplClient);
if (xrpBalance < paymentAmountXrp) {
throw new Error(
`Insufficient XRP balance on ${xrplWallet.address}: have ${xrpBalance} XRP, need ${paymentAmountXrp} XRP`,
);
}
This check covers only the payment amount; the XRPL network also requires the account to keep its base reserve, which is not subtracted here.
If the wallet's spendable balance is close to the reserve, the payment can still fail with tecUNFUNDED_PAYMENT even though this check passes.
Step 1: Send the hash memo on XRPL
The user-side helper function computes the memo, sends the XRPL Payment to the FAssets direct-minting address, and returns everything the executor needs to drive Step 2:
export type HashInstructionUserSide = {
xrplTransactionHash: string;
/** ABI-encoded PackedUserOperation - bytes the executor delivers via _data. */
data: `0x${string}`;
/** Sum of call.value across the UserOp; the executor must forward this as msg.value. */
totalCallValue: bigint;
/** Nonce used in the UserOp; (personalAccount, nonce) identifies the UserOperationExecuted log. */
nonce: bigint;
};
export async function sendHashInstruction({
calls,
amountXrp,
personalAccount,
xrplClient,
xrplWallet,
}: {
calls: Call[];
amountXrp: number;
personalAccount: Address;
xrplClient: Client;
xrplWallet: Wallet;
}): Promise<HashInstructionUserSide> {
const [nonce, coreVaultXrplAddress] = await Promise.all([
getNonce(personalAccount),
getDirectMintingPaymentAddress(),
]);
const { memoData, data } = encodeHashInstructionMemo({
calls,
walletId: 0,
executorFeeUBA: 0n,
sender: personalAccount,
nonce,
});
const totalCallValue = calls.reduce((acc, c) => acc + c.value, 0n);
const transaction = await sendXrplPayment({
destination: coreVaultXrplAddress,
amount: amountXrp,
memos: [{ Memo: { MemoData: memoData.slice(2) } }],
wallet: xrplWallet,
client: xrplClient,
});
return {
xrplTransactionHash: transaction.result.hash,
data,
totalCallValue,
nonce,
};
}
The data field carries the full ABI-encoded PackedUserOperation.
This is what the executor must submit on Flare; the XRPL ledger only ever sees the 32-byte hash.
XRPL payments targeting smart accounts must not use a destination tag. A destination tag forces the FAssets direct minting flow to credit the tag holder, which could let an unrelated party front-run the user's operation.
Step 2: Executor submits the proof and the bytes
The executor takes the XRPL transaction hash, waits for the XRPL ledger to confirm it under the required number of validated ledgers (see Finality on the XRPPayment attestation reference for the exact depth and reasoning), requests an FDC XRPPayment attestation, and submits AssetManagerFXRP.executeDirectMintingWithData with the proof and the user-operation bytes:
export async function executeDirectMintingWithData({
xrplTransactionHash,
data,
value,
xrplClient,
}: {
xrplTransactionHash: string;
data: `0x${string}`;
value: bigint;
xrplClient: Client;
}): Promise<{ hash: `0x${string}`; receipt: TransactionReceipt }> {
const transactionId = (
xrplTransactionHash.startsWith("0x")
? xrplTransactionHash
: `0x${xrplTransactionHash}`
).toLowerCase() as `0x${string}`;
// FDC XRPPayment rejects requests whose XRPL transaction isn't yet buried
// under XRPL_FDC_CONFIRMATIONS validated ledgers (3 on XRPL ~= 12 s).
await waitForXrplFinality({
client: xrplClient,
transactionHash: xrplTransactionHash,
});
// proofOwner binds the proof to the executor's externally owned account so
// the AssetManager's verifyProofOwnership check accepts it on submission.
const { abiEncodedRequest } = await prepareXrpPaymentRequest({
transactionId,
proofOwner: account.address,
verifierBaseUrl: process.env.VERIFIER_URL_TESTNET!,
apiKey: process.env.VERIFIER_API_KEY_TESTNET!,
});
const roundId = await submitAttestationRequest(abiEncodedRequest);
const proof = await retrieveXrpPaymentProofWithRetry(
abiEncodedRequest,
roundId,
);
const assetManagerFxrpAddress = await getAssetManagerFXRPAddress();
const hash = await walletClient.writeContract({
account,
address: assetManagerFxrpAddress,
abi: iDirectMintingExtAbi,
functionName: "executeDirectMintingWithData",
args: [proof, data],
value,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
return { hash, receipt };
}
Two details are critical here:
proofOwner: the FDCXRPPaymentrequest binds the proof to the externally owned account that will eventually submit it.AssetManagerFXRPenforces this throughTransactionAttestation.verifyProofOwnership. Pass the executor's address so the proof can only be used by that executor.value:msg.valueonexecuteDirectMintingWithDatais forwardedAssetManager -> MasterAccountController.handleMintedFAssets -> PersonalAccount.executeUserOp. It must therefore equal the sum ofcall.valueacross the user operation - exactly thetotalCallValuereturned by Step 1.
Step 3: Confirm execution from the receipt
The MasterAccountController runs the user operation inside the executor's transaction, so UserOperationExecuted is already present in the receipt's logs by the time executeDirectMintingWithData returns.
There is no event watcher to start; the script just parses the receipt:
export function findUserOperationExecuted(
receipt: TransactionReceipt,
personalAccount: Address,
nonce: bigint,
): UserOperationExecutedEventType {
const logs = parseEventLogs({
abi: iMemoInstructionsFacetAbi,
eventName: "UserOperationExecuted",
logs: receipt.logs,
});
for (const log of logs) {
const typedLog = log as unknown as UserOperationExecutedEventType;
if (
typedLog.args.personalAccount.toLowerCase() ===
personalAccount.toLowerCase() &&
typedLog.args.nonce === nonce
) {
return typedLog;
}
}
throw new Error(
`UserOperationExecuted not found on receipt ${receipt.transactionHash} ` +
`for (${personalAccount}, ${nonce}). The mint may have been delayed; ` +
"check for DirectMintingDelayed.",
);
}
The error path matters: if the AssetManager hits a rate limit or the minting is otherwise delayed (DirectMintingDelayed), the FAsset transfer is deferred and the user operation does not execute synchronously.
In that case the executor must wait for the delay to clear and re-call executeDirectMintingWithData.
Wiring it Together
The main function chains the three steps:
const userSide = await sendHashInstruction({
calls,
amountXrp: paymentAmountXrp,
personalAccount,
xrplClient,
xrplWallet,
});
const { receipt } = await executeDirectMintingWithData({
xrplTransactionHash: userSide.xrplTransactionHash,
data: userSide.data,
value: userSide.totalCallValue,
xrplClient,
});
const event = findUserOperationExecuted(
receipt,
personalAccount,
userSide.nonce,
);
On mainnet, sendHashInstruction would run on the user's machine, the (data, totalCallValue, xrplTransactionHash) triple would be POST-ed to the executor service, and executeDirectMintingWithData would run there.
findUserOperationExecuted can run on either side once the executor publishes the resulting receipt.
Full Script
The repository with the example is available on GitHub.
Helper functions live in the src/utils directory.
import { encodeFunctionData } from "viem";
import { Client, Wallet } from "xrpl";
import { abi as checkpointAbi } from "./abis/Checkpoint";
import { abi as piggyBankAbi } from "./abis/PiggyBank";
import { abi as noticeBoardAbi } from "./abis/NoticeBoard";
import {
executeDirectMintingWithData,
findUserOperationExecuted,
getPersonalAccountAddress,
sendHashInstruction,
type Call,
} from "./utils/smart-accounts";
import { computeDirectMintingPaymentAmountXrp } from "./utils/fassets";
import { getXrpBalance } from "./utils/xrpl";
// NOTE:(Nik) For this example to work, you first need to faucet C2FLR to your personal account address.
//
// The 0xFE flow is a three-step protocol. This script runs all three steps
// itself for end-to-end demo purposes, but in production they map to two
// independent actors:
// 1. USER SIDE - encode the UserOp, commit `keccak256(userOp)` in the
// 42-byte XRPL memo, send the XRPL Payment.
// 2. EXECUTOR SIDE - fetch an FDC Payment proof for the XRPL transaction
// and call AssetManagerFXRP.executeDirectMintingWithData
// with the proof and the full UserOp bytes.
// 3. CONFIRMATION - the MasterAccountController executes the UserOp inside the executor tx,
// so the receipt's logs already contain
// UserOperationExecuted; no separate watcher needed.
async function main() {
// Net FXRP amount to mint in XRP. Minting + executor fees are fetched from
// AssetManagerFXRP and added on top to form the XRPL payment amount.
const fxrpMintAmount = 10;
const checkpointAddress = "0xEE6D54382aA623f4D16e856193f5f8384E487002";
const piggyBankAddress = "0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42";
const noticeBoardAddress = "0x59D57652BF4F6d97a6e555800b3920Bd775661Dc";
const depositAmount = 1 * 10 ** 18;
const pinNoticeAmount = 1 * 10 ** 18;
const pinNoticeMessage = "Hello World!";
// With 0xFE the XRPL memo is always 42 bytes regardless of call-batch size,
// so all three calls fit into a single payment.
const calls: Call[] = [
{
target: checkpointAddress,
value: BigInt(0),
data: encodeFunctionData({
abi: checkpointAbi,
functionName: "passCheckpoint",
args: [],
}),
},
{
target: piggyBankAddress,
value: BigInt(depositAmount),
data: encodeFunctionData({
abi: piggyBankAbi,
functionName: "deposit",
args: [],
}),
},
{
target: noticeBoardAddress,
value: BigInt(pinNoticeAmount),
data: encodeFunctionData({
abi: noticeBoardAbi,
functionName: "pinNotice",
args: [pinNoticeMessage],
}),
},
];
const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);
const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);
const [personalAccount, paymentAmountXrp] = await Promise.all([
getPersonalAccountAddress(xrplWallet.address),
computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: fxrpMintAmount }),
]);
console.log("Personal account address:", personalAccount, "\n");
console.log("Payment amount (XRP, net mint + fees):", paymentAmountXrp, "\n");
const xrpBalance = await getXrpBalance(xrplWallet.address, xrplClient);
console.log("XRPL wallet XRP balance:", xrpBalance, "\n");
if (xrpBalance < paymentAmountXrp) {
throw new Error(
`Insufficient XRP balance on ${xrplWallet.address}: have ${xrpBalance} XRP, need ${paymentAmountXrp} XRP`,
);
}
// --- 1. USER SIDE -------------------------------------------------------
// Send the XRPL Payment carrying the 32-byte UserOp hash in the memo. The
// full PackedUserOperation bytes (returned as `data`) never go onto XRPL.
const userSide = await sendHashInstruction({
label: "hash-instruction-batch",
calls,
amountXrp: paymentAmountXrp,
personalAccount,
xrplClient,
xrplWallet,
});
// --- 2. EXECUTOR SIDE ---------------------------------------------------
// Fetch the FDC Payment proof for the XRPL transaction and submit it to
// AssetManagerFXRP together with `data`. `totalCallValue` is forwarded as
// msg.value (AssetManager -> MasterAccountController.handleMintedFAssets -> PersonalAccount.call).
const { receipt } = await executeDirectMintingWithData({
xrplTransactionHash: userSide.xrplTransactionHash,
data: userSide.data,
value: userSide.totalCallValue,
xrplClient,
label: "hash-instruction-batch",
});
// --- 3. CONFIRMATION ----------------------------------------------------
// The MasterAccountController executes the UserOp inside the executor transaction, so the
// receipt's logs already contain UserOperationExecuted.
const event = findUserOperationExecuted(
receipt,
personalAccount,
userSide.nonce,
);
console.log("UserOperationExecuted:", event, "\n");
}
void main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Expected output
Personal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9F
Payment amount (XRP, net mint + fees): 10.2
XRPL wallet XRP balance: 1000
[hash-instruction-batch] calls: [
{
target: '0xEE6D54382aA623f4D16e856193f5f8384E487002',
value: 0n,
data: '0x80abd133'
},
{
target: '0x42Ccd4F0aB1C6Fa36BfA37C9e30c4DC4DD94dE42',
value: 1000000000000000000n,
data: '0xd0e30db0'
},
{
target: '0x59D57652BF4F6d97a6e555800b3920Bd775661Dc',
value: 1000000000000000000n,
data: '0x28d106b20000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000'
}
]
[hash-instruction-batch] current nonce: 58n
[hash-instruction-batch] userOpHash: 0x3f2a3cc369ddaaf39a2837744875a32334ddd1bf067741dc766c905944f33c6d
[hash-instruction-batch] _data (1216 bytes): 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000fd2f0eb6b9fa4fe5bb1f7b26fee3c647ed103d9f000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000140...
[hash-instruction-batch] total call.value (native value to attach on executor tx): 2000000000000000000n
[hash-instruction-batch] XRPL transaction hash: FEAC1ABDB81809293E023DB9345715FA3A27949B23132AB9A2417D8F99A876E9
[hash-instruction-batch] Waiting for XRPL transaction to reach 3 confirmations
[hash-instruction-batch] XRPL finality reached: 3 confirmations (txLedger=17528566, validated=17528568)
[hash-instruction-batch] Preparing FDC XRPPayment attestation for txid 0xfeac1abdb81809293e023db9345715fa3a27949b23132ab9a2417d8f99a876e9 (proofOwner=0xF5488132432118596fa13800B68df4C0fF25131d)
Url: https://fdc-verifiers-testnet.flare.network/verifier/xrp/XRPPayment/prepareRequest
Prepared request:
{
attestationType: '0x5852505061796d656e7400000000000000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
requestBody: {
transactionId: '0xfeac1abdb81809293e023db9345715fa3a27949b23132ab9a2417d8f99a876e9',
proofOwner: '0xF5488132432118596fa13800B68df4C0fF25131d'
}
}
Response status is OK
FDC attestation submitted. Round id: 1342615
Waiting for FDC round to finalize...
Round finalized.
[hash-instruction-batch] FDC proof obtained (votingRound=1342615)
[hash-instruction-batch] Calling executeDirectMintingWithData on 0xc1Ca88b937d0b528842F95d5731ffB586f4fbDFA (value=2000000000000000000)
[hash-instruction-batch] executeDirectMintingWithData tx: 0x80dbe245bf727959120104ba3e782b3b633a422f17d15ebc4a94c29b7bbd3d7d
UserOperationExecuted: {
eventName: 'UserOperationExecuted',
args: {
personalAccount: '0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9F',
nonce: 58n
},
address: '0x434936d47503353f06750db1a444dbdc5f0ad37c',
topics: [
'0xf1fb8f9b365735a54cdafe3a27ffbad0a0cf1f35454f0c4c0c4dc68591d484fe',
'0x000000000000000000000000fd2f0eb6b9fa4fe5bb1f7b26fee3c647ed103d9f'
],
data: '0x000000000000000000000000000000000000000000000000000000000000003a',
blockNumber: 30741518n,
transactionHash: '0x80dbe245bf727959120104ba3e782b3b633a422f17d15ebc4a94c29b7bbd3d7d',
transactionIndex: 1,
blockHash: '0x9e15f4337d276a75306e5edf058fb7a8aa800006e54d70174e74adcd7c1d63ac',
logIndex: 12,
removed: false,
blockTimestamp: undefined
}
To continue your Flare Smart Accounts development journey, you can:
- Get the smart account state using the State Lookup guide.
- Read the Custom Instruction overview for the on-chain details.
- Explore the Raw Custom Instruction TypeScript guide.
- Compare the two flows in the Custom Instruction Comparison.
- Dig into the
IMasterAccountControllerreference.