Skip to main content

Payment

The Payment attestation type enables data collection about a transaction, classified as payment on the native chain. The currently supported chain are: BTC, DOGE, and XRP. You can learn more about it in the official specification repo.

We will define a scripts/fdcExample/Payment.ts file that will encapsulate the whole process.

scripts/fdcExample/Payment.s.sol
import { run, web3 } from "hardhat";
import { PaymentRegistryInstance } from "../../typechain-types";
import {
prepareAttestationRequestBase,
submitAttestationRequest,
retrieveDataAndProofBase,
} from "./Base";

const Payment = artifacts.require("PaymentRegistry");

const { VERIFIER_URL_TESTNET, VERIFIER_API_KEY_TESTNET, COSTON2_DA_LAYER_URL } =
process.env;

...

async function main() {
const data = await prepareAttestationRequest(transactionId, inUtxo, utxo);
console.log("Data:", data, "\n");

const abiEncodedRequest = data.abiEncodedRequest;
const roundId = await submitAttestationRequest(abiEncodedRequest);

const proof = await retrieveDataAndProof(abiEncodedRequest, roundId);

const paymentRegistry: PaymentRegistryInstance =
await deployAndVerifyContract();

await interactWithContract(paymentRegistry, proof);
}

main().then((data) => {
process.exit(0);
});

The function names mostly mirror the steps described in the FDC guide.

To bridge the separate executions of the scripts, we will save the relevant data of each script to a file in the dirPath folder. Each succeeding script will then read that file to load the data.

Prepare request

In this guide we will demonstrate how to prepare an attestation request through a verifier server. At the end of the section we will provide a breakdown of the abi encoded request; thus we will demonstrate how it can be constructed manually.

To use the verifier server, we send a request to its prepareRequest endpoint. A JSON request to the verifier follows the same structure for all attestation types, with field values varying between types.

Required Fields

  • attestationType is the UTF8 hex string encoding of the attestation type name, zero-padded to 32 bytes.
  • sourceId is the UTF8 hex string encoding of the data source identifier name, zero-padded to 32 bytes.
  • requestBody is different for each attestation type.

In the case of Payment, requestBody is a JSON containing the fields:

  • transactionId: id of the transaction; as bytes32
  • inUtxo: UTXO chains support multiple source addresses, so this is the index of the address considered, as uint256; for non-UTXO chains this should always be 0
  • utxo: UTXO chains support multiple receiving addresses, so this is the index of the address considered, as uint256; for non-UTXO chains this should always be 0

Reference Documentation

Example Values

  • transactionId: the above address 9421cbb7f195df66d16703442a408261fa973514a0bd9dfc680f10eb3942d11f
  • inUtxo: non-default 0
  • utxo: non-default 0
  • urlTypeBase: string xrp
    • Replace xrp with doge or btc for other chains.
scripts/fdcExample/Payment.ts
// Request data
const transactionId =
"2A3E7C7F6077B4D12207A9F063515EACE70FBBF3C55514CD8BD659D4AB721447";
const inUtxo = "0";
const utxo = "0";

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

Encoding Functions

To encode values into UTF8 hex:

  • toUtf8HexString: Converts a string to UTF8 hex.
  • toHex: Zero-right-pads the string to 32 bytes.

These functions are included in the Base library within the example repository, but they can also be defined locally in your contract or script.

scripts/fdcExample/Base.ts
function toHex(data: string) {
var result = "";
for (var i = 0; i < data.length; i++) {
result += data.charCodeAt(i).toString(16);
}
return result.padEnd(64, "0");
}
scripts/fdcExample/Base.ts
function toUtf8HexString(data: string) {
return "0x" + toHex(data);
}

Because of the console.log commands it will produce JSON strings that represent valid requests; we can then pass this to the interactive verifier to check what the response will be.

The process of posting a request to a verifier server is identical for all attestation types. It differs only in values used. For that reason we define a base function that the prepareAttestationRequest function will call.

The prepareAttestationRequestBase function formulates a request for the verifier server, and posts it to the given URL.

scripts/fdcExample/Base.ts
async function prepareAttestationRequestBase(
url: string,
apiKey: string,
attestationTypeBase: string,
sourceIdBase: string,
requestBody: any,
) {
console.log("Url:", url, "\n");
const attestationType = toUtf8HexString(attestationTypeBase);
const sourceId = toUtf8HexString(sourceIdBase);

const request = {
attestationType: attestationType,
sourceId: sourceId,
requestBody: requestBody,
};
console.log("Prepared request:\n", request, "\n");

const response = await fetch(url, {
method: "POST",
headers: {
"X-API-KEY": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (response.status != 200) {
throw new Error(
`Response status is not OK, status ${response.status} ${response.statusText}\n`,
);
}
console.log("Response status is OK\n");

return await response.json();
}

In the example repository, it is once again included within the Base library file.

We construct the URL by appending to the verifier address https://fdc-verifiers-testnet.flare.network/ the path verifier/btc/Payment/prepareRequest. If we were using another source, we would replace the string eth with sgb or flr accordingly (we would also have to replace testETH with testSGB or testFLR). Thus, the function that prepares the verifier request looks like:

scripts/fdcExample/Payment.ts
async function prepareAttestationRequest(
transactionId: string,
inUtxo: string,
utxo: string,
) {
const requestBody = {
transactionId: transactionId,
inUtxo: inUtxo,
utxo: utxo,
};

const url = `${verifierUrlBase}verifier/${urlTypeBase}/Payment/prepareRequest`;
const apiKey = VERIFIER_API_KEY_TESTNET ?? "";

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

Understanding the abiEncodedRequest. If everything went right, the abiEncodedRequest should look something like this (minus the line breaks - we split it after the 0x symbol and then after every 64 characters (32 bytes), for the sake of clarity).

0x
4164647265737356616c69646974790000000000000000000000000000000000
7465737442544300000000000000000000000000000000000000000000000000
7d2ef938d4ffd2392f588bf46563e07ab885b15fead91c1bb99b16f465b71a68
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000022
6d6739503966347772397737633173674665695443356f4d4c59584363326337
6873000000000000000000000000000000000000000000000000000000000000

Let's break it down line by line:

  • First line: toUtf8HexString("Payment")
  • Second line: toUtf8HexString("testETH")
  • Third line: message integrity code (MIC), a hash of the whole response salted with a string "Flare", ensures the integrity of the attestation
  • Remaining lines: ABI encoded Payment.RequestBody Solidity struct

What this demonstrates is that, with some effort, the abiEncodedRequest can be constructed manually.

Submit request to FDC

This step transitions from off-chain request preparation to on-chain interaction with the FDC protocol. Now, we submit the validated request to the blockchain using deployed smart contracts. We will submit the validated request to the blockchain using deployed official Flare smart contracts. To streamline the process of accessing these, the Flare smart contracts periphery package is shipped with the ContractRegistry library.

The ContractRegistry library allows us to

We define a Helpers contract that will give us access to the following contracts:

  • FdcHub: for posting the request to
  • FdcRequestFeeConfigurations: calculates the fee of the request
  • FlareSystemsManager: for calculating the round ID
  • Relay: confirms the round has finalized
contracts/fdcExample/Helpers.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {console} from "hardhat/console.sol";
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
import {IFdcHub} from "@flarenetwork/flare-periphery-contracts/coston2/IFdcHub.sol";
import {IFdcRequestFeeConfigurations} from
"@flarenetwork/flare-periphery-contracts/coston2/IFdcRequestFeeConfigurations.sol";
import {IFlareSystemsManager} from "@flarenetwork/flare-periphery-contracts/coston2/IFlareSystemsManager.sol";
import {IRelay} from "@flarenetwork/flare-periphery-contracts/coston2/IRelay.sol";

contract Helpers {
function getFdcHub() public view returns (IFdcHub) {
return ContractRegistry.getFdcHub();
}

function getFdcRequestFeeConfigurations() public view returns (IFdcRequestFeeConfigurations) {
return ContractRegistry.getFdcRequestFeeConfigurations();
}

function getFlareSystemsManager() public view returns (IFlareSystemsManager) {
return ContractRegistry.getFlareSystemsManager();
}

function getRelay() public view returns (IRelay) {
return ContractRegistry.getRelay();
}
}

We expose the Helpers contract through the following function.

scripts/fdcExample/Base.ts
async function getHelpers() {
const helpers: HelpersInstance = await Helpers.new();
return helpers;
}

To submit the attestation request, we first access the deployed FdcHub contract. We determine the fee for our attestation type, and then request the attestation of the FDC, paying the required fee. Lastly, we calculate the voting round Id from the transaction that carried the attestation request; we will need it to query the data and proof.

scripts/fdcExample/Base.ts
async function submitAttestationRequest(abiEncodedRequest: string) {
const fdcHub = await getFdcHub();

const requestFee = await getFdcRequestFee(abiEncodedRequest);

const transaction = await fdcHub.requestAttestation(abiEncodedRequest, {
value: requestFee,
});
console.log("Submitted request:", transaction.tx, "\n");

const roundId = await calculateRoundId(transaction);
console.log(
`Check round progress at: https://${hre.network.name}-systems-explorer.flare.rocks/voting-epoch/${roundId}?tab=fdc\n`,
);
return roundId;
}
scripts/fdcExample/Base.ts
async function getFdcHub() {
const helpers: HelpersInstance = await getHelpers();
const fdcHubAddress: string = await helpers.getFdcHub();
return await FdcHub.at(fdcHubAddress);
}

The request fee is obtained from the fdcRequestFeeConfigurations contract. We once again connect to the fdcRequestFeeConfigurations contract through the ContractRegistry library.

scripts/fdcExample/Base.ts
async function getFdcRequestFee(abiEncodedRequest: string) {
const helpers: HelpersInstance = await getHelpers();
const fdcRequestFeeConfigurationsAddress: string =
await helpers.getFdcRequestFeeConfigurations();
const fdcRequestFeeConfigurations: IFdcRequestFeeConfigurationsInstance =
await FdcRequestFeeConfigurations.at(fdcRequestFeeConfigurationsAddress);
return await fdcRequestFeeConfigurations.getRequestFee(abiEncodedRequest);
}

The round ID is calculate from the timestamp of the block, containing the transaction requesting attestation. We first subtract from the block timestamp the timestamp of the first voting epoch. Then, we divide the number by the duration of the voting epoch (90 seconds). Instead of hard-coding them, we retrieve these values from another official Flare contract, the flareSystemsManager.

scripts/fdcExample/Base.ts
async function calculateRoundId(transaction: any) {
const blockNumber = transaction.receipt.blockNumber;
const block = await ethers.provider.getBlock(blockNumber);
const blockTimestamp = BigInt(block!.timestamp);

const flareSystemsManager: IFlareSystemsManagerInstance =
await getFlareSystemsManager();
const firsVotingRoundStartTs = BigInt(
await flareSystemsManager.firstVotingRoundStartTs(),
);
const votingEpochDurationSeconds = BigInt(
await flareSystemsManager.votingEpochDurationSeconds(),
);

console.log("Block timestamp:", blockTimestamp, "\n");
console.log("First voting round start ts:", firsVotingRoundStartTs, "\n");
console.log(
"Voting epoch duration seconds:",
votingEpochDurationSeconds,
"\n",
);

const roundId = Number(
(blockTimestamp - firsVotingRoundStartTs) / votingEpochDurationSeconds,
);
console.log("Calculated round id:", roundId, "\n");
console.log(
"Received round id:",
Number(await flareSystemsManager.getCurrentVotingEpochId()),
"\n",
);
return roundId;
}

We obtain the flareSystemsManager contract through the ContractRegistry library and the previously defined Helpers contract as well.

scripts/fdcExample/Base.ts
async function getFlareSystemsManager() {
const helpers: HelpersInstance = await getHelpers();
const flareSystemsManagerAddress: string =
await helpers.getFlareSystemsManager();
return await FlareSystemsManager.at(flareSystemsManagerAddress);
}

Retrieve data and proof

To retrieve the data and proof, we must first wait for the voting round in which the attestation request was submitted to finalize; this takes no more than 180 seconds, but is on average much less. After the round has been finalized, we post a request to a DA Layer provider.

We can check if the request was submitted successfully on the AttestationRequests page on the Flare Systems Explorer website. To check if the round has been finalized, go to Finalizations page.

You can learn more about how the FDC protocol works, check here.

Because the process includes waiting for the voting round to finalize, we prepare a sleep function. The function pauses the execution of the script for a given number of milliseconds.

scripts/fdcExample/Base.ts
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

The only difference between the retrieveDataAndProof functions of all six attestation types is the URL of the DA Layer server. For that reason, we will define as separate retrieveDataAndProofBase function that will handle most of the logic. The function waits for the round to finalize - rechecking every 10 seconds if necessary. Then, it prepares a proof request, and posts it to the DA Layer server. Because it might take a few seconds for the server to generate the proof, the function ensures that the response actually contains a sufficient response, and retries otherwise.

scripts/fdcExample/Base.ts
async function retrieveDataAndProofBase(
url: string,
abiEncodedRequest: string,
roundId: number,
) {
console.log("Waiting for the round to finalize...");
// We check every 10 seconds if the round is finalized
const relay: IRelayInstance = await getRelay();
while (!(await relay.isFinalized(200, roundId))) {
await sleep(10000);
}
console.log("Round finalized!\n");

const request = {
votingRoundId: roundId,
requestBytes: abiEncodedRequest,
};
console.log("Prepared request:\n", request, "\n");

await sleep(10000);
var proof = await postRequestToDALayer(url, request, true);
console.log("Waiting for the DA Layer to generate the proof...");
while (proof.response_hex == undefined) {
await sleep(5000);
proof = await postRequestToDALayer(url, request, false);
}
console.log("Proof generated!\n");

console.log("Proof:", proof, "\n");
return proof;
}

We access the Flare's official Relay contract with a helper function.

scripts/fdcExample/Base.ts
async function getRelay() {
const helpers: HelpersInstance = await getHelpers();
const relayAddress: string = await helpers.getRelay();
return await IRelay.at(relayAddress);
}

The following function posts a proof request to the DA Layer.

scripts/fdcExample/Base.ts
async function postRequestToDALayer(
url: string,
request: any,
watchStatus: boolean = false,
) {
const response = await fetch(url, {
method: "POST",
headers: {
// "X-API-KEY": "",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (watchStatus && response.status != 200) {
throw new Error(
`Response status is not OK, status ${response.status} ${response.statusText}\n`,
);
} else if (watchStatus) {
console.log("Response status is OK\n");
}
return await response.json();
}

The main prepare the URL of the DA Layer's proof-by-request-raw endpoint. We contact this specific endpoint, because it return the abi encoded IPayment.Response struct, and is thus unambiguous.

scripts/fdcExample/Payment.ts
async function retrieveDataAndProof(
abiEncodedRequest: string,
roundId: number,
) {
const url = `${COSTON2_DA_LAYER_URL}api/v1/fdc/proof-by-request-round-raw`;
console.log("Url:", url, "\n");
return await retrieveDataAndProofBase(url, abiEncodedRequest, roundId);
}

The response the DA Layer server returns has the following fields:

  • The field attestationType holds the UTF8 encoded hex string of the attestation type name, padded to 32 bytes. Thus, it should match the value of the attestationType parameter in the Prepare the request step. In our case, that value is 0x4164647265737356616c69646974790000000000000000000000000000000000.

  • The array proofs holds the Merkle proofs of our attestation request.

  • Lastly, responseHex is the ABI encoding of the chosen attestation type response struct. In this case, it is the IPayment.Response struct.

We can ascertain the form of the proof request, as well as examine the response in advance, trough the interactive documentation of the DA Layer server.

An example complete proof response and decoded IPayment.Response.

An example DA Layer response for a request using the data provided in this example is:

{
response_hex: '0x
5061796d656e7400000000000000000000000000000000000000000000000000
7465737458525000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000e6c2d
00000000000000000000000000000000000000000000000000000000a019d806
2a3e7c7f6077b4d12207a9f063515eace70fbbf3c55514cd8bd659d4ab721447
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000048f822
0000000000000000000000000000000000000000000000000000000067ac9486
7f5b4967a9fbe9b447fed6d4e3699051516b6afe5f94db2e77ccf86470bfd74d
a1475e9840d916c22f494c0dc25428d2affb5ae1f496efc82bbb59d46a336779
cd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82
cd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82
0000000000000000000000000000000000000000000000000000000005f5e10c
0000000000000000000000000000000000000000000000000000000005f5e10c
0000000000000000000000000000000000000000000000000000000005f5e100
0000000000000000000000000000000000000000000000000000000005f5e100
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000000',
attestation_type: '0x5061796d656e7400000000000000000000000000000000000000000000000000',
proof: [
'0xe1f98d39167eab17b2157c06efb80530b161d5eb15c439fc476e3242e30b3ac1',
'0x23a8ffdb2cbaf0e2f3653923a159150f8d4c3ad5160f9e127cc9797ba233e6c2',
'0xd756b90367b336e127f0759a1457825b4c2bf9011b71b56e15d9fcb7ff735ec8',
'0xc881d1566868a986aef2bda47e9ab6dafeb8241bde5f5d53235837595829a5ea'
]
}

The proof field is dependent on the round in which the attestation request was submitted; it contains proofs for all of the requests submitted in that round. In the case of a single attestation request it is an empty list [] (the proof is the merkle root itself).

The decoded IPayment.Response struct is:

[
attestationType: '0x5061796d656e7400000000000000000000000000000000000000000000000000',
sourceId: '0x7465737458525000000000000000000000000000000000000000000000000000',
votingRound: '945197',
lowestUsedTimestamp: '2686048262',
requestBody: [
'0x2a3e7c7f6077b4d12207a9f063515eace70fbbf3c55514cd8bd659d4ab721447',
'0',
'0',
transactionId: '0x2a3e7c7f6077b4d12207a9f063515eace70fbbf3c55514cd8bd659d4ab721447',
inUtxo: '0',
utxo: '0'
],
responseBody: [
'4782114',
'1739363462',
'0x7f5b4967a9fbe9b447fed6d4e3699051516b6afe5f94db2e77ccf86470bfd74d',
'0xa1475e9840d916c22f494c0dc25428d2affb5ae1f496efc82bbb59d46a336779',
'0xcd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82',
'0xcd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82',
'100000012',
'100000012',
'100000000',
'100000000',
'0x0000000000000000000000000000000000000000000000000000000000000000',
true,
'0',
blockNumber: '4782114',
blockTimestamp: '1739363462',
sourceAddressHash: '0x7f5b4967a9fbe9b447fed6d4e3699051516b6afe5f94db2e77ccf86470bfd74d',
sourceAddressesRoot: '0xa1475e9840d916c22f494c0dc25428d2affb5ae1f496efc82bbb59d46a336779',
receivingAddressHash: '0xcd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82',
intendedReceivingAddressHash: '0xcd582d251987f15ecb29b69c2e02051479e84c176e39cbbdf04a4d0ef89bcf82',
spentAmount: '100000012',
intendedSpentAmount: '100000012',
receivedAmount: '100000000',
intendedReceivedAmount: '100000000',
standardPaymentReference: '0x0000000000000000000000000000000000000000000000000000000000000000',
oneToOne: true,
status: '0'
]
]

Use the data

We will now define a simple contract, that will demonstrate how the data can be used onchain. The contract will receive data and proof of a Payment transaction, and store it into an array of special Payment structs. It will do so only if the transaction is valid.

src/fdcExample/Payment.sol
struct Payment {
uint64 blockNumber;
uint64 blockTimestamp;
bytes32 sourceAddressHash;
bytes32 receivingAddressHash;
int256 spentAmount;
bytes32 standardPaymentReference;
uint8 status;
}

The code for the contract as follows.

src/fdcExample/Payment.sol

contract PaymentRegistry is IPaymentRegistry {
Payment[] public verifiedPayments;

function isPaymentProofValid(
IPayment.Proof calldata transaction
) public view returns (bool) {
// Use the library to get the verifier contract and verify that this transaction was proved by state connector
IFdcVerification fdc = ContractRegistry.getFdcVerification();
console.log("transaction: %s\n", FdcStrings.toJsonString(transaction));
// return true;
return fdc.verifyPayment(transaction);
}

function registerPayment(IPayment.Proof calldata _transaction) external {
// 1. FDC Logic
// Check that this Payment has indeed been confirmed by the FDC
require(isPaymentProofValid(_transaction), "Invalid transaction proof");

// 2. Business logic
Payment memory provedPayment = Payment(
_transaction.data.responseBody.blockNumber,
_transaction.data.responseBody.blockTimestamp,
_transaction.data.responseBody.sourceAddressHash,
_transaction.data.responseBody.receivingAddressHash,
_transaction.data.responseBody.spentAmount,
_transaction.data.responseBody.standardPaymentReference,
_transaction.data.responseBody.status
);

verifiedPayments.push(provedPayment);
}
}

Verify proof

FDC optimizes on-chain storage costs by implementing a hybrid data verification system. Instead of storing complete datasets on-chain, it stores only Merkle proofs, while maintaining the actual data through trusted off-chain providers. This approach significantly reduces gas costs while preserving data integrity.

When requested, data providers supply the original data along with its corresponding Merkle proof. The protocol verifies data authenticity by comparing the provided Merkle proof against the on-chain Merkle root. A successful match confirms the data's integrity and authenticity within the FDC system.

While data verification is optional if you trust your data provider, FDC ensures transparency by making verification possible at any time. This capability is crucial for maintaining system integrity and allowing users to independently verify data when needed, particularly in production environments.

FDC provides verification functionality through the FdcVerification contract.

We then access the FdcVerification contract through the ContractRegistry, and feed it the proof. If we proof is valid, the function verifyPayment will return true, otherwise false.

We deploy and verify this contract with the deployAndVerifyContract function in the scripts/fdcExample/Payment.ts file.

scripts/fdcExample/Payment.ts
async function deployAndVerifyContract() {
const args: any[] = [];
const paymentRegistry: PaymentRegistryInstance = await Payment.new(...args);
try {
await run("verify:verify", {
address: paymentRegistry.address,
constructorArguments: args,
});
} catch (e: any) {
console.log(e);
}
console.log("Payment deployed to", paymentRegistry.address, "\n");
return paymentRegistry;
}

Interact with contract

We define an additional function that allows us to interact with the just deployed contract. The interactWithContract function also takes the proof retrieved in the previous step as an argument. It abi decodes the response_hex value to an IPayment.Response struct. From that and the array of proofs, it constructs an IPayment.Proof object, on which it call the registerPayment function of the PaymentRegistry contract deployed above. The contract verifies the payment, and the script prints it to the console.

scripts/fdcExample/Payment.ts
async function interactWithContract(
paymentRegistry: PaymentRegistryInstance,
proof: any,
) {
console.log("Proof hex:", proof.response_hex, "\n");

// A piece of black magic that allows us to read the response type from an artifact
const IPaymentVerification = await artifacts.require("IPaymentVerification");
const responseType =
IPaymentVerification._json.abi[0].inputs[0].components[1];
console.log("Response type:", responseType, "\n");

const decodedResponse = web3.eth.abi.decodeParameter(
responseType,
proof.response_hex,
);
console.log("Decoded proof:", decodedResponse, "\n");
const transaction = await paymentRegistry.registerPayment({
merkleProof: proof.proof,
data: decodedResponse,
});
console.log("Transaction:", transaction.tx, "\n");
console.log(
"Verified payment:",
await paymentRegistry.verifiedPayments(0),
"\n",
);
}

We can run the whole script by calling the following console command.

yarn hardhat run scripts/fdcExample/Payment.ts