Skip to main content

Build your first FTSOv2 app

This guide is for developers who want to build an FTSOv2 application using Foundry. In this guide, you will learn how to:

  • Create a contract to read the price of FLR/USD from FTSOv2 using flare-periphery-contracts.

  • Compile your contract using Foundry forge.

  • Deploy your contract to Flare Testnet Coston2, and interact with it using Foundry cast.

Prerequisites

Ensure you have the following tools installed:

Create a Foundry project

  1. Create a new directory to hold your app and navigate inside it:

    mkdir ftsov2-app && cd ftsov2-app
  2. Initialize an empty Foundry project:

    forge init

    This creates several subdirectories like src and test, with some sample contracts in them.

  3. Verify the setup by running the sample test:

    forge test

    The output should look similar to this:

    [⠢] Compiling...
    No files changed, compilation skipped

    Running 2 tests for test/Counter.t.sol:CounterTest
    [PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 27864, ~: 28409)
    [PASS] test_Increment() (gas: 28379)
    Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.30ms

    Ran 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)
  4. Clean up by deleting the sample contracts and tests:

    rm -r src/* test/*

Install the Flare periphery

Install the Flare periphery package to interact with FTSOv2 contracts.

  1. Initialize an npm project and install the Flare periphery package:

    npm init -y
    npm i @flarenetwork/flare-periphery-contracts
  2. Create a file remappings.txt in the main directory, and add the following remappings to ensure Foundry can locate the installed packages:

    remappings.txt
    @flarenetwork/flare-periphery-contracts/=node_modules/@flarenetwork/flare-periphery-contracts/
    @openzeppelin/contracts/=node_modules/@openzeppelin/contracts

Create and compile a contract

Now, you can create a contract that consumes data from FTSOv2.

  1. Create a contract file src/FtsoV2FeedConsumer.sol, and add the following code to it:

    src/FtsoV2FeedConsumer.sol
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity >=0.8.0 <0.9.0;

    import {console2} from "forge-std/Test.sol";
    import {FtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/FtsoV2Interface.sol";
    import {IFeeCalculator} from "@flarenetwork/flare-periphery-contracts/coston2/IFeeCalculator.sol";

    contract FtsoV2FeedConsumer {
    FtsoV2Interface internal ftsoV2;
    IFeeCalculator internal feeCalc;
    bytes21[] public feedIds;
    bytes21 public flrUsdId;
    uint256 public fee;

    constructor(address _ftsoV2, address _feeCalc, bytes21 _flrUsdId) {
    ftsoV2 = FtsoV2Interface(_ftsoV2);
    feeCalc = IFeeCalculator(_feeCalc);
    flrUsdId = _flrUsdId;
    feedIds.push(_flrUsdId);
    }

    function checkFees() external returns (uint256 _fee) {
    fee = feeCalc.calculateFeeByIds(feedIds);
    return fee;
    }

    function getFlrUsdPrice() external payable returns (uint256, int8, uint64) {
    (uint256 feedValue, int8 decimals, uint64 timestamp) = ftsoV2
    .getFeedById{value: msg.value}(flrUsdId);

    if (fee != msg.value) {
    console2.log("msg.value %i doesn't match fee %i", msg.value, fee);
    } else {
    console2.log("msg.value matches fee");
    }

    console2.log("feedValue %i", feedValue);
    console2.log("decimals %i", decimals);
    console2.log("timestamp %i", timestamp);
    return (feedValue, decimals, timestamp);
    }
    }
  2. To ensure everything is set up correctly, compile the contract by running:

    forge build

    The output should indicate that the compilation was successful.

    [⠊] Compiling...
    [⠃] Compiling 27 files with Solc 0.8.27
    [⠊] Solc 0.8.27 finished in 853.78ms
    Compiler run successful!

Write tests

Before deploying, it's important to write tests for your contract.

  1. Create a test file test/FtsoV2FeedConsumer.t.sol, and add the following code:

    test/FtsoV2FeedConsumer.t.sol
    // SPDX-License-Identifier: UNLICENSED
    pragma solidity >=0.8.0 <0.9.0;

    import "forge-std/Test.sol";
    import {FtsoV2FeedConsumer} from "../src/FtsoV2FeedConsumer.sol";

    contract MockFtsoV2 {
    function getFeedById(
    bytes21 /*_feedId*/
    ) external payable returns (uint256, int8, uint64) {
    return (150000, 7, uint64(block.timestamp));
    }
    }

    contract MockFeeCalculator {
    function calculateFeeByIds(
    bytes21[] memory /*_feedIds*/
    ) external pure returns (uint256) {
    return 0;
    }
    }

    contract FtsoV2FeedConsumerTest is Test {
    FtsoV2FeedConsumer public feedConsumer;
    MockFtsoV2 public mockFtsoV2;
    MockFeeCalculator public mockFeeCalc;
    bytes21 constant flrUsdId =
    bytes21(0x01464c522f55534400000000000000000000000000);

    function setUp() public {
    mockFtsoV2 = new MockFtsoV2();
    mockFeeCalc = new MockFeeCalculator();
    feedConsumer = new FtsoV2FeedConsumer(
    address(mockFtsoV2),
    address(mockFeeCalc),
    flrUsdId
    );
    }

    function testCheckFees() public {
    assertEq(feedConsumer.checkFees(), 0, "Feed value mismatch");
    }

    function testGetFlrUsdPrice() public {
    (uint256 feedValue, int8 decimals, uint64 timestamp) = feedConsumer
    .getFlrUsdPrice{value: 0}();
    assertEq(feedValue, 150000, "Feed value mismatch");
    assertEq(decimals, 7, "Decimals mismatch");
    assertApproxEqAbs(
    timestamp,
    uint64(block.timestamp),
    2,
    "Timestamp mismatch"
    );
    }
    }
  2. Run the tests:

    forge test

    You should see a successful test result like this:

    [⠊] Compiling...
    [⠘] Compiling 27 files with Solc 0.8.27
    [⠃] Solc 0.8.27 finished in 797.51ms
    Compiler run successful!

    Ran 2 tests for test/FtsoV2FeedConsumer.t.sol:FtsoV2FeedConsumerTest
    [PASS] testCheckFees() (gas: 21085)
    [PASS] testGetFlrUsdPrice() (gas: 25610)
    Logs:
    msg.value matches fee
    feedValue 150000
    decimals 7
    timestamp 1

    Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.72ms (2.91ms CPU time)

    Ran 1 test suite in 122.65ms (7.72ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Deploy and interact with the contract

You can now deploy your contract to Flare Testnet Coston2.

  1. Generate a new wallet using the cast:

    cast wallet new

    The output will look something like:

    Successfully created new keypair.
    Address: 0x3f6BdD26f2AE4e77AcDfA1FA24B2774ed93984B4
    Private key: 0x84cf77b009a92777f75b49864e4166ddcaf8f3f5f119a19b226ab362a0cf7bf5
  2. Store your wallet details and the RPC URL as environment variables:

    danger
    • Never share your private keys.
    • Never put your private keys in source code.
    • Never commit private keys to a Git repository.
    export ACCOUNT=<address above>
    export ACCOUNT_PRIVATE_KEY=<private key above>
    export RPC_URL="https://coston2-api.flare.network/ext/C/rpc"
  3. Use the Coston2 Faucet to get some testnet C2FLR tokens. You can verify that the 100 C2FLR has arrived in your wallet:

    cast balance $ACCOUNT -r $RPC_URL -e
  4. The final step before deploying is to set the constructor arguments with the address of FtsoV2 and FeeCalculator on Flare Testnet Coston2 and the feed ID of FLR/USD:

    export FTSOV2_COSTON2=0x3d893C53D9e8056135C26C8c638B76C8b60Df726
    export FEECALCULATOR_COSTON2=0x88A9315f96c9b5518BBeC58dC6a914e13fAb13e2
    export FLRUSD_FEED_ID=0x01464c522f55534400000000000000000000000000

    You can now deploy the contract:

    forge create src/FtsoV2FeedConsumer.sol:FtsoV2FeedConsumer --private-key $ACCOUNT_PRIVATE_KEY --rpc-url $RPC_URL --constructor-args $FTSOV2_COSTON2 $FEECALCULATOR_COSTON2 $FLRUSD_FEED_ID

    If the deployment is successful, the output will display the contract address, save that for later use:

    [⠊] Compiling...
    [⠘] Compiling 24 files with Solc 0.8.27
    [⠃] Solc 0.8.27 finished in 733.41ms
    Compiler run successful!
    Deployer: 0x3f6BdD26f2AE4e77AcDfA1FA24B2774ed93984B4
    Deployed to: 0x80Ee4091348d9fA4B4A84Eb525c25049EbDa6152
    Transaction hash: 0x38604a643695959dd9fa5547d95610fb0b7393c7e8358079f47ed4bdb53c9a8f
    export DEPLOYMENT_ADDRESS=<deployed to address above>
  5. Use cast to interact with the contract, note that this command uses the environment variables defined in the sections above.:

    cast send --private-key $ACCOUNT_PRIVATE_KEY --rpc-url $RPC_URL -j --value 0 $DEPLOYMENT_ADDRESS "getFlrUsdPrice()"
    Expected output of the command above.
    {
    "status": "0x1",
    "cumulativeGasUsed": "0x1cbab",
    "logs": [
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x00000000000000000000000098b8e9b5830f04fe3b8d56a2f8455e337037ba280000000000000000000000000000000000000000000000000000000000004231",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x0",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x0000000000000000000000004f52e61907b0ed9f26b88f16b2510a4ca524d6d00000000000000000000000000000000000000000000000000000000000003099",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x1",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x000000000000000000000000d2a1bb23eb350814a30dd6f9de78bb2c8fdd9f1d0000000000000000000000000000000000000000000000000000000000003b68",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x2",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x0000000000000000000000006892bdbbb14e1c9bd46bf31e7bac94d038fc82a6000000000000000000000000000000000000000000000000000000000000422d",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x3",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x000000000000000000000000bd33bdff04c357f7fc019e72d0504c24cf4aa0100000000000000000000000000000000000000000000000000000000000008f11",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x4",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x000000000000000000000000a90db6d10f856799b10ef2a77ebcbf460ac71e520000000000000000000000000000000000000000000000000000000000004e9c",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x5",
    "removed": false
    },
    {
    "address": "0x1000000000000000000000000000000000000002",
    "topics": [
    "0xe7aa66356adbd5e839ef210626f6d8f6f72109c17fadf4c4f9ca82b315ae79b4"
    ],
    "data": "0x0000000000000000000000000b162ca3acf3482d3357972e12d794434085d839000000000000000000000000000000000000000000000000000000000000e5a6",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "logIndex": "0x6",
    "removed": false
    }
    ],
    "logsBloom": "0x00020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000400000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "type": "0x2",
    "transactionHash": "0x3fdc9cf00456a7878476877b6f8ae5c994dd3c224ca792f965f718340fd98402",
    "transactionIndex": "0x0",
    "blockHash": "0x94f50404f8205caff551ef2b08d20afc4c080bd7b8231cd3941f1a7a6b1b80dd",
    "blockNumber": "0xb2b972",
    "gasUsed": "0x1cbab",
    "effectiveGasPrice": "0x6fc23ac00",
    "from": "0x3f6bdd26f2ae4e77acdfa1fa24b2774ed93984b4",
    "to": "0x80ee4091348d9fa4b4a84eb525c25049ebda6152",
    "contractAddress": null
    }

You can see the transaction using the Coston2 Explorer by searching for its transactionHash.

Congratulations! You've built your first app using FTSOv2.

What's next

Learn how to read feeds offchain using JavaScript, Python, Rust and Go, or learn how to change quote feed with an onchain Solidity contract.