Skip to main content

Web2Json

The Web2Json attestation type enables data collection from an arbitrary Web2 source. You can learn more about it in the official specification repo.

We will now demonstrate how the FDC protocol can be used to collect the data of a given Star Wars API request. It will be a GET request to the URL https://swapi.info/api/peaople/3. The same procedure works for all public APIs.

In this guide, we will follow the steps outlined under User Workflow in the FDC overview.

Our implementation requires handling the FDC voting round finalization process. To manage this, we will create separate scripts in script/fdcExample/Web2Json.s.sol that handle different stages of the validation process:

script/fdcExample/Web2Json.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
...

string constant attestationTypeName = "Web2Json";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
...
}

contract SubmitAttestationRequest is Script {
...
}

contract RetrieveDataAndProof is Script {
...
}

contract Deploy is Script {
...
}
...

The names of included contracts 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.

info

The code used is the guide is mostly take from the Flare Foundry starter repository.

Prepare request

The JSON request to the verifier is of the same form for all attestation types, but the values of the fields differ between them. It contains the following fields.

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 Web2Json, requestBody is a JSON containing the fields:

  • url: url of the data source; as string
  • httpMethod: one of GET, POST, PUT, PATCH and DELETE
  • headers: request headers as a stringified JSON; {} if no headers are required (defaults to {"Content-Type": "application/json"})
  • queryParams: request query parameters as a stringified JSON; {} if no query parameters are required,
  • body: request body as a stringified JSON; {} if no body is required,
  • postProcessJq: JQ filter to postprocess the json data received from the URL; as string
  • abiSignature: ABI signature of the Solidity struct that will be used to decode the data; as string

Reference Documentation

Example Values

  • url: the above address https://swapi.info/api/people/3
  • httpMethod: GET
  • headers: {}, which defaults to value {"Content-Type": "application/json"}
  • queryParams: {}
  • body: {}
  • postProcessJq: {name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split(\\"/\\") | .[-1] | tonumber)}
  • abiSignature:
{\\"components\\": [
{\\"internalType\\": \\"string\\", \\"name\\": \\"name\\", \\"type\\": \\"string\\"},
{\\"internalType\\": \\"uint256\\", \\"name\\": \\"height\\", \\"type\\": \\"uint256\\"},
{\\"internalType\\": \\"uint256\\", \\"name\\": \\"mass\\", \\"type\\": \\"uint256\\"},
{\\"internalType\\": \\"uint256\\", \\"name\\": \\"numberOfFilms\\", \\"type\\": \\"uint256\\"},
{\\"internalType\\": \\"uint256\\", \\"name\\": \\"uid\\", \\"type\\": \\"uint256\\"}
],
\\"name\\": \\"task\\",\\"type\\": \\"tuple\\"}

Encoding Functions

To encode values into UTF8 hex:

  • toUtf8HexString: Converts a string to UTF8 hex.
  • toHexString: 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.

scrip/fdcExample/Base.s.sol
function toHexString(
bytes memory data
) public pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < data.length; i++) {
str[2 + i * 2] = alphabet[uint(uint8(data[i] >> 4))];
str[3 + i * 2] = alphabet[uint(uint8(data[i] & 0x0f))];
}
return string(str);
}
script/fdcExample/Base.s.sol
function toUtf8HexString(
string memory _string
) internal pure returns (string memory) {
string memory encodedString = toHexString(
abi.encodePacked(_string)
);
uint256 stringLength = bytes(encodedString).length;
require(stringLength <= 64, "String too long");
uint256 paddingLength = 64 - stringLength + 2;
for (uint256 i = 0; i < paddingLength; i++) {
encodedString = string.concat(encodedString, "0");
}
return encodedString;
}

We also define a helper function for formatting data into a JSON string.

scrip/fdcExample/Base.s.sol
function prepareAttestationRequest(
string memory attestationType,
string memory sourceId,
string memory requestBody
) internal view returns (string[] memory, string memory) {
// We read the API key from the .env file
string memory apiKey = vm.envString("VERIFIER_API_KEY");

// Preparing headers
string[] memory headers = prepareHeaders(apiKey);
// Preparing body
string memory body = prepareBody(
attestationType,
sourceId,
requestBody
);

console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);
console.log("body: %s\n", body);
return (headers, body);
}

function prepareHeaders(
string memory apiKey
) internal pure returns (string[] memory) {
string[] memory headers = new string[](2);
headers[0] = string.concat('"X-API-KEY": ', apiKey);
headers[1] = '"Content-Type": "application/json"';
return headers;
}

function prepareBody(
string memory attestationType,
string memory sourceId,
string memory body
) internal pure returns (string memory) {
return
string.concat(
'{"attestationType": ',
'"',
attestationType,
'"',
', "sourceId": ',
'"',
sourceId,
'"',
', "requestBody": ',
body,
"}"
);
}

In the example repository, these are once again included within the Base library file.

Thus, the part of the script that prepares the verifier request looks like:

scrip/fdcExample/Web2Json.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import {console} from "dependencies/forge-std-1.9.5/src/console.sol";
import {Script} from "dependencies/forge-std-1.9.5/src/Script.sol";
import {Base} from "./Base.s.sol";
...

string constant attestationTypeName = "Web2Json";
string constant dirPath = "data/";

contract PrepareAttestationRequest is Script {
using Surl for *;

// Setting request data
// string public apiUrl = "https://swapi.dev/api/people/3/";
string public apiUrl = "https://swapi.info/api/people/3";
string public httpMethod = "GET";
// Defaults to "Content-Type": "application/json"
string public headers = '{\\"Content-Type\\":\\"text/plain\\"}';
string public queryParams = "{}";
string public body = "{}";
string public postProcessJq =
'{name: .name, height: .height, mass: .mass, numberOfFilms: .films | length, uid: (.url | split(\\"/\\") | .[-1] | tonumber)}';
string public abiSignature =
'{\\"components\\": [{\\"internalType\\": \\"string\\", \\"name\\": \\"name\\", \\"type\\": \\"string\\"},{\\"internalType\\": \\"uint256\\", \\"name\\": \\"height\\", \\"type\\": \\"uint256\\"},{\\"internalType\\": \\"uint256\\", \\"name\\": \\"mass\\", \\"type\\": \\"uint256\\"},{\\"internalType\\": \\"uint256\\", \\"name\\": \\"numberOfFilms\\", \\"type\\": \\"uint256\\"},{\\"internalType\\": \\"uint256\\", \\"name\\": \\"uid\\", \\"type\\": \\"uint256\\"}],\\"name\\": \\"task\\",\\"type\\": \\"tuple\\"}';

string public sourceName = "PublicWeb2";

function prepareRequestBody(
string memory url,
string memory httpMethod,
string memory headers,
string memory queryParams,
string memory body,
string memory postProcessJq,
string memory abiSignature
) private pure returns (string memory) {
return
string.concat(
'{"url": "',
url,
'","httpMethod": "',
httpMethod,
'","headers": "',
headers,
'","queryParams": "',
queryParams,
'","body": "',
body,
'","postProcessJq": "',
postProcessJq,
'","abiSignature": "',
abiSignature,
'"}'
);
}

function run() external {
// Preparing request data
string memory attestationType = Base.toUtf8HexString(
attestationTypeName
);
string memory sourceId = Base.toUtf8HexString(sourceName);
string memory requestBody = prepareRequestBody(
apiUrl,
httpMethod,
headers,
queryParams,
body,
postProcessJq,
abiSignature
);
(string[] memory headers, string memory body) = Base
.prepareAttestationRequest(attestationType, sourceId, requestBody);

// string memory baseUrl = "https://testnet-verifier-fdc-test.aflabs.org/";
string memory baseUrl = vm.envString("WEB2JSON_VERIFIER_URL_TESTNET");
string memory url = string.concat(
baseUrl,
"Web2Json",
"/prepareRequest"
);
console.log("url: %s", url);

// Posting the attestation request
(, bytes memory data) = url.post(headers, body);

Base.AttestationResponse memory response = Base.parseAttestationRequest(
data
);

// Writing abiEncodedRequest to a file
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_abiEncodedRequest"),
StringsBase.toHexString(response.abiEncodedRequest),
true
);
}
}

...

The code above differs slightly from the starter example. But, if we remove the ellipses ... signifying missing code, we can still run the script.

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 the response.

We can run the script by calling the following commands in the console.

source .env
forge script script/fdcExample/Web2Json.s.sol:PrepareAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast  --ffi

The prerequisite for this is that the .env file is not missing the PRIVATE KEY and COSTON2_RPC_URL values. The script can also access other chains; that can be achieved by replacing the --rpc-url value with COSTON_RPC_URL, FLARE_RPC_URL, or SONGBIRD_RPC_URL.

Post request to verifier

To post a request to a verifier server, we use the surl package. We place using Surl for *; at the start of our PostRequest contract, and then call its post method on the verifier URL.

scrip/fdcExample/Web2Json.s.sol
(, bytes memory data) = url.post(headers, body);

We construct the URL by appending to the verifier address https://fdc-verifiers-testnet.flare.network/ the path verifier/btc/Web2Json/prepareRequest. We can do so dynamically with the following code.

scrip/fdcExample/Web2Json.s.sol
string memory baseUrl = "https://fdc-verifiers-testnet.flare.network/";
string memory url = string.concat(
baseUrl,
"verifier/",
baseSourceName,
"/",
attestationTypeName,
"/prepareRequest"
);
console.log("url: %s", url);
string memory requestBody = string.concat(
'{"addressStr": "',
addressStr,
'"}'
);

Lastly, we parse the return data from the verifier server. Using the Foundry parseJson shortcode, and a custom struct AttestationResponse, we decode the returned data and extract from it the ABI encoded request.

scrip/fdcExample/Base.s.sol
function parseAttestationRequest(
bytes memory data
) internal pure returns (AttestationResponse memory) {
string memory dataString = string(data);
bytes memory dataJson = vm.parseJson(dataString);

AttestationResponse memory response = abi.decode(
dataJson,
(AttestationResponse)
);

console.log("response status: %s\n", response.status);
console.log("response abiEncodedRequest: ");
console.logBytes(response.abiEncodedRequest);
console.log("\n");

return response;
}
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
576562324a736f6e000000000000000000000000000000000000000000000000
5075626c69635765623200000000000000000000000000000000000000000000
c69254586c046b1394616cbf98dc97ef115a492ff5dbcd9392cd775a102b2acc
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000160
00000000000000000000000000000000000000000000000000000000000001a0
00000000000000000000000000000000000000000000000000000000000001e0
0000000000000000000000000000000000000000000000000000000000000220
00000000000000000000000000000000000000000000000000000000000002c0
000000000000000000000000000000000000000000000000000000000000001f
68747470733a2f2f73776170692e696e666f2f6170692f70656f706c652f3300
0000000000000000000000000000000000000000000000000000000000000003
4745540000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000001d
7b22436f6e74656e742d54797065223a22746578742f706c61696e227d000000
0000000000000000000000000000000000000000000000000000000000000002
7b7d000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
7b7d000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000078
7b6e616d653a202e6e616d652c206865696768743a202e6865696768742c206d
6173733a202e6d6173732c206e756d6265724f6646696c6d733a202e66696c6d
73207c206c656e6774682c207569643a20282e75726c207c2073706c69742822
2f2229207c202e5b2d315d207c20746f6e756d626572297d0000000000000000
0000000000000000000000000000000000000000000000000000000000000173
7b22636f6d706f6e656e7473223a205b7b22696e7465726e616c54797065223a
2022737472696e67222c20226e616d65223a20226e616d65222c202274797065
223a2022737472696e67227d2c7b22696e7465726e616c54797065223a202275
696e74323536222c20226e616d65223a2022686569676874222c202274797065
223a202275696e74323536227d2c7b22696e7465726e616c54797065223a2022
75696e74323536222c20226e616d65223a20226d617373222c20227479706522
3a202275696e74323536227d2c7b22696e7465726e616c54797065223a202275
696e74323536222c20226e616d65223a20226e756d6265724f6646696c6d7322
2c202274797065223a202275696e74323536227d2c7b22696e7465726e616c54
797065223a202275696e74323536222c20226e616d65223a2022756964222c20
2274797065223a202275696e74323536227d5d2c226e616d65223a2022746173
6b222c2274797065223a20227475706c65227d00000000000000000000000000

Let's break it down line by line:

  • First line: toUtf8HexString("Web2Json")
  • Second line: toUtf8HexString("PublicWeb2")
  • 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 Web2Json.RequestBody Solidity struct

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

We write the abiEncodedRequest to a file (data/Web2Json_abiEncodedRequest.txt) to it in the next step.

scrip/fdcExample/Web2Json.s.sol
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_abiEncodedRequest"),
StringsBase.toHexString(response.abiEncodedRequest),
true
);

Submit request to FDC

This step transitions from off-chain request preparation to onchain interaction with the FDC protocol. Now, we submit the validated request to the blockchain using deployed smart contracts.

Submit request

The entire submission process requires only five key steps:

scrip/fdcExample/Base.s.sol
function submitAttestationRequest(
bytes memory abiEncodedRequest
) internal {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
abiEncodedRequest
);
console.log("request fee: %s\n", requestFee);
vm.stopBroadcast();

vm.startBroadcast(deployerPrivateKey);

IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");
fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
vm.stopBroadcast();
}

Step-by-Step Breakdown

  1. Load Private Key The private key is read from the .env file using Foundry's envUint function:
       uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
  1. Obtain Request Fee We retrieve the required requestFee from the FdcRequestFeeConfigurations contract:
        IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(
abiEncodedRequest
);

This is done in a separate broadcast to ensure requestFee is available before submitting the request.

  1. Access FdcHub Contract Using the ContractRegistry library (from flare-periphery), we fetch the FdcHub contract:
   IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
console.log("\n");
  1. Submit the Attestation Request We send the attestation request with the required fee:
 fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
  1. Calculate the Voting Round Number To determine the voting round in which the attestation request is processed, we query the FlareSystemsManager contract:
       // Calculating roundId
IFlareSystemsManager flareSystemsManager = ContractRegistry
.getFlareSystemsManager();

uint32 roundId = flareSystemsManager.getCurrentVotingEpochId();
console.log("roundId: %s\n", Strings.toString(roundId));

This can be done within the existing broadcast or in a new one (as done in the demo repository for better code organization).

Wait for response

We wait for the round to finalize. This takes no more than 180 seconds.

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

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

Prepare proof request

We prepare the proof request in a similar manner as in the step Prepare the request, by string concatenation. We import two new variables from the .env file; the URL of a verifier server and the corresponding API key.

scrip/fdcExample/Web2Json.s.sol
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

Also, by repeatedly using the Foundry shortcode vm.readLine, we read the data, saved to a file in the previous step, to variables.

scrip/fdcExample/Web2Json.s.sol
string memory requestBytes = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_abiEncodedRequest",
".txt"
)
);
string memory votingRoundId = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_votingRoundId",
".txt"
)
);

The code is as follows.

scrip/fdcExample/Web2Json.s.sol
contract RetrieveDataAndProof is Script {
using Surl for *;

function run() external {
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");

string memory requestBytes = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_abiEncodedRequest",
".txt"
)
);
string memory votingRoundId = vm.readLine(
string.concat(
dirPath,
attestationTypeName,
"_votingRoundId",
".txt"
)
);

console.log("votingRoundId: %s\n", votingRoundId);
console.log("requestBytes: %s\n", requestBytes);

string[] memory headers = Base.prepareHeaders(apiKey);
string memory body = string.concat(
'{"votingRoundId":',
votingRoundId,
',"requestBytes":"',
requestBytes,
'"}'
);
console.log("body: %s\n", body);
console.log(
"headers: %s",
string.concat("{", headers[0], ", ", headers[1]),
"}\n"
);


...
}
}

Post proof request to DA Layer

We post the proof request to a chosen DA Layer provider server also with the same code as we did in the previous step.

scrip/fdcExample/Web2Json.s.sol
string memory url = string.concat(
daLayerUrl,
// "api/v0/fdc/get-proof-round-id-bytes"
"api/v1/fdc/proof-by-request-round-raw"
);
console.log("url: %s\n", url);

(, bytes memory data) = Base.postAttestationRequest(url, headers, body);

Parsing the returned data requires the definition of an auxiliary struct.

scrip/fdcExample/Base.s.sol
struct ParsableProof {
bytes32 attestationType;
bytes32[] proofs;
bytes responseHex;
}

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 0x576562324a736f6e000000000000000000000000000000000000000000000000.

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 IWeb2Json.Response struct. We retrieve this data as follows.

scrip/fdcExample/Web2Json.s.sol
bytes memory dataJson = parseData(data);
ParsableProof memory proof = abi.decode(dataJson, (ParsableProof));

IWeb2Json.Response memory proofResponse = abi.decode(
proof.responseHex,
(IWeb2Json.Response)
);
An example complete proof response and decoded IEVMTransaction.Response.

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

{
response_hex: '0x
0000000000000000000000000000000000000000000000000000000000000020
576562324a736f6e000000000000000000000000000000000000000000000000
5075626c69635765623200000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000fa347
0000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000c0
0000000000000000000000000000000000000000000000000000000000000520
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000160
00000000000000000000000000000000000000000000000000000000000001a0
00000000000000000000000000000000000000000000000000000000000001e0
0000000000000000000000000000000000000000000000000000000000000220
00000000000000000000000000000000000000000000000000000000000002c0
000000000000000000000000000000000000000000000000000000000000001f
68747470733a2f2f73776170692e696e666f2f6170692f70656f706c652f3300
0000000000000000000000000000000000000000000000000000000000000003
4745540000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000001d
7b22436f6e74656e742d54797065223a22746578742f706c61696e227d000000
0000000000000000000000000000000000000000000000000000000000000002
7b7d000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
7b7d000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000078
7b6e616d653a202e6e616d652c206865696768743a202e6865696768742c206d
6173733a202e6d6173732c206e756d6265724f6646696c6d733a202e66696c6d
73207c206c656e6774682c207569643a20282e75726c207c2073706c69742822
2f2229207c202e5b2d315d207c20746f6e756d626572297d0000000000000000
0000000000000000000000000000000000000000000000000000000000000173
7b22636f6d706f6e656e7473223a205b7b22696e7465726e616c54797065223a
2022737472696e67222c20226e616d65223a20226e616d65222c202274797065
223a2022737472696e67227d2c7b22696e7465726e616c54797065223a202275
696e74323536222c20226e616d65223a2022686569676874222c202274797065
223a202275696e74323536227d2c7b22696e7465726e616c54797065223a2022
75696e74323536222c20226e616d65223a20226d617373222c20227479706522
3a202275696e74323536227d2c7b22696e7465726e616c54797065223a202275
696e74323536222c20226e616d65223a20226e756d6265724f6646696c6d7322
2c202274797065223a202275696e74323536227d2c7b22696e7465726e616c54
797065223a202275696e74323536222c20226e616d65223a2022756964222c20
2274797065223a202275696e74323536227d5d2c226e616d65223a2022746173
6b222c2274797065223a20227475706c65227d00000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000100
0000000000000000000000000000000000000000000000000000000000000020
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000006
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000005
52322d4432000000000000000000000000000000000000000000000000000000',
attestation_type: '0x576562324a736f6e000000000000000000000000000000000000000000000000',
proof: [
'0x564e7596bf828952e3fbc21c889effced7c31a90fc0d5572a0b812faf87a1738',
'0x738b3f104a0a56b2fbae32ea0e853a5b4529e0e4b915c89063e69bb61c03dde5'
]
}

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

Verify proof

FDC optimizes onchain storage costs by implementing a hybrid data verification system. Instead of storing complete datasets onchain, it stores only Merkle proofs, while maintaining the actual data through trusted offchain 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 onchain 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. To verify address validity, we first format our data using the IEVMTransaction.Proof struct, which contains both the Merkle proof and the response data.

scrip/fdcExample/Web2Json.s.sol
IWeb2Json.Proof memory _proof = IWeb2Json.Proof(
proof.proofs,
proofResponse
);

We then access the FdcVerification contract through the ContractRegistry, and feed it the proof. If the proof is valid, the function verifyWeb2Json will return true, otherwise false. As before, we wrap the whole thing into a broadcast environment, using the PRIVATE_KEY variable from our .env file.

scrip/fdcExample/Web2Json.s.sol
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

bool isValid = ContractRegistry
.getFdcVerification()
.verifyWeb2Json(proof);
console.log("proof is valid: %s\n", StringsBase.toString(isValid));

vm.stopBroadcast();

In actuality, we will only verify the proof within a deployed contract, which we will define in the next step. What we will do here instead is, we will save the proof to a file so that it can be later loaded into a variable. The code that does this is as follows.

scrip/fdcExample/EVMTransaction.s.sol
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_proof"),
StringsBase.toHexString(abi.encode(_proof)),
true
);

Use the data

We will now define a simple contract, that will demonstrate how the data can be used onchain. The contract will receive character data from the Star Wars API, and store it in a StarWarsCharacter struct. It will do so only if the proof is valid.

src/fdcExample/Web2Json.sol
struct StarWarsCharacter {
string name;
uint256 numberOfMovies;
uint256 apiUid;
uint256 bmi;
}

We will also need a DataTransportObject struct, that will allow us to decode the data.

src/fdcExample/Web2Json.sol
struct DataTransportObject {
string name;
uint256 height;
uint256 mass;
uint256 numberOfMovies;
uint256 apiUid;
}

First, we define an interface that the contract will inherit from. We do so, so that we may contact the contract later through a script.

src/fdcExample/Web2Json.sol
interface IStarWarsCharacterList {
function addCharacter(IWeb2Json.Proof calldata data) external;
function getAllCharacters()
external
view
returns (StarWarsCharacter[] memory);
}

The interface exposes the two functions that a user might call, addCharacter and getAllCharacters. We now define the contract as follows.

src/fdcExample/Web2Json.sol

contract StarWarsCharacterList {
mapping(uint256 => StarWarsCharacter) public characters;
uint256[] public characterIds;

function isWeb2JsonProofValid(
IWeb2Json.Proof calldata _proof
) private view returns (bool) {
// Inline the check for now until we have an official contract deployed
return
ContractRegistry.auxiliaryGetIWeb2JsonVerification().verifyWeb2Json(
_proof
);
}

function addCharacter(IWeb2Json.Proof calldata data) public {
require(isWeb2JsonProofValid(data), "Invalid proof");

DataTransportObject memory dto = abi.decode(
data.data.responseBody.abi_encoded_data,
(DataTransportObject)
);

require(characters[dto.apiUid].apiUid == 0, "Character already exists");

StarWarsCharacter memory character = StarWarsCharacter({
name: dto.name,
numberOfMovies: dto.numberOfMovies,
apiUid: dto.apiUid,
bmi: (dto.mass * 100 * 100) / (dto.height * dto.height)
});

characters[dto.apiUid] = character;
characterIds.push(dto.apiUid);
}

function getAllCharacters()
public
view
returns (StarWarsCharacter[] memory)
{
StarWarsCharacter[] memory result = new StarWarsCharacter[](
characterIds.length
);
for (uint256 i = 0; i < characterIds.length; i++) {
result[i] = characters[characterIds[i]];
}
return result;
}
}

We deploy the contract through a simple script. The script creates a new StarWarsCharacterList contract and writes its address to a file (data/Web2Json_listenerAddress.txt).

scrip/fdcExample/DeployContract.s.sol
contract DeployContract is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);

StarWarsCharacterList characterList = new StarWarsCharacterList();
address _address = address(characterList);

vm.stopBroadcast();

Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_address"),
StringsBase.toHexString(abi.encodePacked(_address)),
true
);
}
}

We deploy the contract with the following console command.

forge script script/fdcExample/Web2Json.s.sol:DeployContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast --verify --ffi

Lastly, we define a script that interacts with the above contract. It first reads the ABI-encoded proof data, and the contract address, from files. Then, it connects to the above contract at the saved address (this is why we require the interface). With that, it can call the registerWeb2Json method of the contract.

script/fdcExample/Web2Json.s.sol
contract InteractWithContract is Script {
function run() external {
string memory addressString = vm.readLine(
string.concat(dirPath, attestationTypeName, "_address", ".txt")
);
address _address = vm.parseAddress(addressString);
string memory proofString = vm.readLine(
string.concat(dirPath, attestationTypeName, "_proof", ".txt")
);
bytes memory proofBytes = vm.parseBytes(proofString);
IWeb2Json.Proof memory proof = abi.decode(proofBytes, (IWeb2Json.Proof));
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
IStarWarsCharacterList characterList = IStarWarsCharacterList(_address);
characterList.addCharacter(proof);
vm.stopBroadcast();
}
}

We run this script with the console command:

forge script script/fdcExample/Web2Json.s.sol:InteractWithContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --etherscan-api-key $FLARE_API_KEY --broadcast --ffi