Official Flare Foundry documentation with complete setup instructions, step-by-step breakdowns, and beginner-friendly explanations.
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Clone the Flare Foundry starter
git clone https://github.com/flare-foundation/flare-foundry-starter
cd flare-foundry-starter
# Install dependencies
forge installCreate a .env file in the project root:
# .env - Foundry Configuration
PRIVATE_KEY=0xYourPrivateKeyHere
COSTON2_RPC_URL=https://coston2-api.flare.network/ext/C/rpc
FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc
VERIFIER_API_KEY=00000000-0000-0000-0000-000000000000
WEB2JSON_VERIFIER_URL_TESTNET=https://web2json-verifier-test.flare.rocks/
COSTON2_DA_LAYER_URL=https://ctn2-data-availability.flare.network/
X_API_KEY=your_da_layer_api_key# Build all contracts
forge buildThe Web2Json attestation type enables data collection from an arbitrary Web2 source. You can learn more about it in the official specification.
Info: The Web2Json attestation type is currently only available on the Flare Testnet Coston2.
We will now demonstrate how the FDC protocol can be used to collect the data of a given Star Wars API request. The request we will be making is https://swapi.info/api/people/3. The same procedure works for all public APIs.
In this guide, we will follow the steps outlined in the FDC overview. We will define separate scripts in script/fdcExample/Web2Json.s.sol that handle different stages of the validation process.
Simple Explanation: Foundry uses a modular approach where each step is a separate contract. Think of it like having different specialists for each job in a factory assembly line.
Factory Assembly Line Analogy:
• PrepareAttestationRequest: Design department (creates the blueprint)
• SubmitAttestationRequest: Shipping department (sends to blockchain)
• RetrieveDataAndProof: Quality control (verifies the product)
• Deploy: Manufacturing (creates the final product)
// 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/";
// Each contract handles one specific step
contract PrepareAttestationRequest is Script {
// Creates the request blueprint
}
contract SubmitAttestationRequest is Script {
// Sends request to blockchain
}
contract RetrieveDataAndProof is Script {
// Gets verified data back
}
contract Deploy is Script {
// Deploys our application contract
}Why Multiple Contracts? This modular approach makes the code more maintainable and allows running individual steps independently. Each contract focuses on one specific responsibility.
Simple Explanation: This is where we create a detailed “shopping list“ that tells Flare exactly what data we want and how to process it.
Tells Flare “I want Web2 data“ → Converted to hex: “Web2Json“
Tells Flare “From public websites“ → Converted to hex: “PublicWeb2“
The requestBody is where we specify exactly what data to fetch and how to process it:
{
"url": "Where to get the data (website address)",
"httpMethod": "How to fetch it (GET, POST, etc.)",
"headers": "Any special headers needed",
"queryParams": "URL parameters to include",
"body": "Data to send for POST requests",
"postProcessJq": "Which fields to extract and how to format them",
"abiSignature": "How to package the data for blockchain use"
}Here's the actual configuration we use to fetch R2-D2's data:
// Inside PrepareAttestationRequest contract
string public apiUrl = "https://swapi.info/api/people/3";
string public httpMethod = "GET";
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\"}';Simple Explanation: Blockchain computers don't understand normal text. These functions translate human-readable words into computer-readable hex codes.
Language Translation Analogy:
• Human: “Web2Json“ (English words)
• Computer: “0x576562324a736f6e0000...“ (Computer language)
Flare provides two key functions in their Base library to handle this conversion:
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);
}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;
}Input: "Web2Json"
Step 1: Convert to bytes → [0x57, 0x65, 0x62, 0x32, 0x4a, 0x73, 0x6f, 0x6e]
Step 2: Convert to hex → "0x576562324a736f6e"
Step 3: Add padding → "0x576562324a736f6e000..."
Input: "PublicWeb2"
Output: "0x5075626c696357656232000..."Simple Explanation: Now we package everything together into a proper HTTP request that we can send to Flare's verifier service.
Shipping Package Analogy:
• Headers: Shipping label with destination and security info
• Body: The actual contents of your package
• API Key: Your security clearance for the shipping company
Flare provides helper functions to build the request properly:
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]), "}");
console.log("body: %s", 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,
"}"
);
}{
"attestationType": "0x576562324a736f6e000000000000000000000000000000000000000000000000",
"sourceId": "0x5075626c6963576562320000000000000000000000000000000000000000000000",
"requestBody": {
"url": "https://swapi.info/api/people/3",
"httpMethod": "GET",
"headers": "{\"Content-Type\":\"text/plain\"}",
"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\"}"
}
}source .env
forge script script/fdcExample/Web2Json.s.sol:PrepareAttestationRequest \
--private-key $PRIVATE_KEY \
--rpc-url $COSTON2_RPC_URL \
--etherscan-api-key $FLARE_RPC_API_KEY \
--broadcast \
--ffiWhat Happens: This script creates the request and saves the abiEncodedRequest to a file (data/Web2Json_abiEncodedRequest.txt) for the next step.
Simple Explanation: Now we take our prepared request and actually submit it to the Flare blockchain. This is like paying for and shipping your package.
Shipping Process Analogy:
• Check shipping cost: See how much gas fee is required
• Pay and ship: Submit transaction with the fee
• Get tracking number: Receive transaction hash and round ID
This step involves interacting with Flare's smart contracts to submit our request:
function submitAttestationRequest(bytes memory abiEncodedRequest) internal {
// Step 1: Load private key and check request fee
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
IFdcRequestFeeConfigurations fdcRequestFeeConfigurations = ContractRegistry
.getFdcRequestFeeConfigurations();
uint256 requestFee = fdcRequestFeeConfigurations.getRequestFee(abiEncodedRequest);
console.log("request fee: %s", requestFee);
vm.stopBroadcast();
// Step 2: Submit the actual request
vm.startBroadcast(deployerPrivateKey);
IFdcHub fdcHub = ContractRegistry.getFdcHub();
console.log("fcdHub address:");
console.log(address(fdcHub));
fdcHub.requestAttestation{value: requestFee}(abiEncodedRequest);
vm.stopBroadcast();
// Step 3: Calculate voting round
IFlareSystemsManager flareSystemsManager = ContractRegistry.getFlareSystemsManager();
uint32 roundId = flareSystemsManager.getCurrentVotingEpochId();
console.log("roundId: %s", Strings.toString(roundId));
}Flare uses a “ContractRegistry“ system that acts like a universal remote control for all Flare services:
// These are like different "buttons" on your remote control:
ContractRegistry.getFdcHub() // "Submit request" button
ContractRegistry.getFdcRequestFeeConfigurations() // "Check price" button
ContractRegistry.getFlareSystemsManager() // "Check time" button
ContractRegistry.getRelay() // "Verify completion" buttonforge script script/fdcExample/Web2Json.s.sol:SubmitAttestationRequest \
--private-key $PRIVATE_KEY \
--rpc-url $COSTON2_RPC_URL \
--etherscan-api-key $FLARE_RPC_API_KEY \
--broadcast \
--ffiWhat to Expect: You'll see the request fee, transaction hash, and voting round ID. The script saves the round ID to data/Web2Json_votingRoundId.txt.
Simple Explanation: Flare's network needs time to verify the data. We wait for the voting round to complete, then retrieve the cryptographic proof.
Quality Control Analogy:
• Waiting period: Factory quality control checks the product
• Verification: Multiple validators confirm data is authentic
• Proof generation: Create certificate of authenticity
90 seconds per voting round
1-3 minutes total wait
Use Flare Systems Explorer
After waiting, we ask the Data Availability (DA) Layer for the verified data and proof:
contract RetrieveDataAndProof is Script {
using Surl for *;
function run() external {
// Load environment variables and saved data
string memory daLayerUrl = vm.envString("COSTON2_DA_LAYER_URL");
string memory apiKey = vm.envString("X_API_KEY");
string memory requestBytes = vm.readLine("data/Web2Json_abiEncodedRequest.txt");
string memory votingRoundId = vm.readLine("data/Web2Json_votingRoundId.txt");
// Prepare the proof request
string[] memory headers = Base.prepareHeaders(apiKey);
string memory body = string.concat(
'{"votingRoundId":', votingRoundId,
',"requestBytes":"', requestBytes, '"}'
);
// Send request to DA Layer
string memory url = string.concat(daLayerUrl, "api/v1/fdc/proof-by-request-round-raw");
(, bytes memory data) = Base.postAttestationRequest(url, headers, body);
// Parse the response
bytes memory dataJson = parseData(data);
ParsableProof memory proof = abi.decode(dataJson, (ParsableProof));
// Save proof for later use
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_proof"),
StringsBase.toHexString(abi.encode(_proof)),
true
);
}
}{
"attestationType": "0x576562324a736f6e00000000000...",
"proofs": [
"0x4305b2025b90e3316dfdad...",
"0x2bdf5b10f6027a520f1e...",
"0xff12aaa4fcb39d21c50...",
"0xadd7610ccf4a2fcea7..."
],
"responseHex": "0x00000000000000000..."
}forge script script/fdcExample/Web2Json.s.sol:RetrieveDataAndProof \
--private-key $PRIVATE_KEY \
--rpc-url $COSTON2_RPC_URL \
--etherscan-api-key $FLARE_RPC_API_KEY \
--broadcast \
--ffiSimple Explanation: This is where we check that the data we received is authentic and hasn't been tampered with.
Security Seal Analogy:
• Merkle Proof: Like a tamper-evident security seal
• On-chain verification: Checking the seal against official records
• Gas optimization: Store only the proof, not the data
FDC uses a clever system to save gas costs while maintaining security:
// FDC's gas optimization strategy:
// 1. Store only Merkle proofs on-chain (cheap)
// 2. Keep actual data off-chain (expensive to store)
// 3. Verify proofs match on-chain records
function verifyWeb2Json(IWeb2Json.Proof memory _proof) internal view returns (bool) {
// Format the proof for verification
IWeb2Json.Proof memory formattedProof = IWeb2Json.Proof(_proof.proofs, _proof.data);
// Ask Flare: "Is this proof valid?"
bool isValid = ContractRegistry.getFdcVerification().verifyWeb2Json(formattedProof);
console.log("proof is valid: %s", StringsBase.toString(isValid));
return isValid;
}Storing proofs is much cheaper than storing full data on-chain
Anyone can verify data authenticity at any time
Simple Explanation: Now we create a smart contract that actually uses the verified Star Wars data. This is our final application.
Collection Display Analogy:
• Data structures: Like display cases and labels for your collectibles
• Verification: Security system that only accepts authenticated items
• Storage: Your actual collection display
We'll create a Star Wars character collection that only accepts verified data:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
// What we want to store permanently
struct StarWarsCharacter {
string name;
uint256 numberOfMovies;
uint256 apiUid;
uint256 bmi; // Calculated from verified data!
}
// Temporary structure for data transport
struct DataTransportObject {
string name;
uint256 height;
uint256 mass;
uint256 numberOfMovies;
uint256 apiUid;
}
// Our main collection contract
contract StarWarsCharacterList {
mapping(uint256 => StarWarsCharacter) public characters;
uint256[] public characterIds;
// STEP 1: Verify the proof is authentic
function isWeb2JsonProofValid(IWeb2Json.Proof calldata _proof)
private view returns (bool)
{
return ContractRegistry.getFdcVerification().verifyWeb2Json(_proof);
}
// STEP 2: Add a new character with verification
function addCharacter(IWeb2Json.Proof calldata data) public {
// First, verify the proof is valid
require(isWeb2JsonProofValid(data), "Invalid proof");
// STEP 3: Unpack the verified data
DataTransportObject memory dto = abi.decode(
data.data.responseBody.abi_encoded_data,
(DataTransportObject)
);
// STEP 4: Check for duplicates
require(characters[dto.apiUid].apiUid == 0, "Character already exists");
// STEP 5: Calculate BMI from verified height & mass
uint256 bmi = (dto.mass * 100 * 100) / (dto.height * dto.height);
// STEP 6: Create and store our character
StarWarsCharacter memory character = StarWarsCharacter({
name: dto.name,
numberOfMovies: dto.numberOfMovies,
apiUid: dto.apiUid,
bmi: bmi // Derived data from verified sources!
});
characters[dto.apiUid] = character;
characterIds.push(dto.apiUid);
}
// STEP 7: View the entire collection
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;
}
}// What Flare verifies (DataTransportObject):
{
"name": "R2-D2",
"height": 96, // 96 cm
"mass": 32, // 32 kg
"numberOfMovies": 7,
"apiUid": 3
}
// What we store (StarWarsCharacter):
{
"name": "R2-D2",
"numberOfMovies": 7,
"apiUid": 3,
"bmi": 3472 // (32 * 100 * 100) / (96 * 96) = 3472
}contract DeployContract is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Deploy our collection contract
StarWarsCharacterList characterList = new StarWarsCharacterList();
address _address = address(characterList);
vm.stopBroadcast();
// Save address for interaction
Base.writeToFile(
dirPath,
string.concat(attestationTypeName, "_address"),
StringsBase.toHexString(abi.encodePacked(_address)),
true
);
}
}# Deploy the contract
forge script script/fdcExample/Web2Json.s.sol:DeployContract \
--private-key $PRIVATE_KEY \
--rpc-url $COSTON2_RPC_URL \
--etherscan-api-key $FLARE_RPC_API_KEY \
--broadcast \
--verify \
--ffi
Live terminal
Simple Explanation: Final step! We take our verified R2-D2 data and add it to our on-chain collection.
This script loads all our saved data and interacts with the deployed contract:
contract InteractWithContract is Script {
function run() external {
// Load saved data from files
string memory addressString = vm.readLine("data/Web2Json_address.txt");
address _address = vm.parseAddress(addressString);
string memory proofString = vm.readLine("data/Web2Json_proof.txt");
bytes memory proofBytes = vm.parseBytes(proofString);
IWeb2Json.Proof memory proof = abi.decode(proofBytes, (IWeb2Json.Proof));
// Connect to our deployed contract
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
IStarWarsCharacterList characterList = IStarWarsCharacterList(_address);
// Add R2-D2 to the collection!
characterList.addCharacter(proof);
vm.stopBroadcast();
}
}# Add the verified character to your collection
forge script script/fdcExample/Web2Json.s.sol:InteractWithContract \
--private-key $PRIVATE_KEY \
--rpc-url $COSTON2_RPC_URL \
--etherscan-api-key $FLARE_RPC_API_KEY \
--broadcast \
--ffiHere's the complete sequence of commands to run the entire Web2JSON workflow with Foundry:
# Step 1: Prepare the attestation request
forge script script/fdcExample/Web2Json.s.sol:PrepareAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi
# Step 2: Submit to blockchain
forge script script/fdcExample/Web2Json.s.sol:SubmitAttestationRequest --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi
# Wait 2-4 minutes for round finalization...
# Step 3: Retrieve the proof
forge script script/fdcExample/Web2Json.s.sol:RetrieveDataAndProof --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffi
# Step 4: Deploy your contract
forge script script/fdcExample/Web2Json.s.sol:DeployContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --verify --ffi
# Step 5: Use the verified data
forge script script/fdcExample/Web2Json.s.sol:InteractWithContract --private-key $PRIVATE_KEY --rpc-url $COSTON2_RPC_URL --broadcast --ffiCongratulations! You've now mastered the complete Foundry Web2JSON workflow. You've learned how to bring real-world data to the blockchain trustlessly using Flare's FDC protocol with Foundry.