Understanding Chainlink

Aaron RebeloAaron Rebelo
15 min read

Source: https://docs.chain.link/getting-started/conceptual-overview

Understanding the basics

I'm writing this article by reading through the chainlink documentation and simplifying challenging concepts with the help of chatGPT. This article will be a one-stop shop for understanding everything about Chainlink. done rush through it, take your time while reading it :)

Blockchain by itself is a deterministic system. so to get values from the outside, it has to use something called an oracle. Think of it as a collection of nodes that access data, pass it through a system and give you a singular value, why do we need this? as different nodes in a blockchain might fetch different values, causing there to be different values stored in a blockchain. which is not right. For example, if there is a function to get a random number, different nodes might end up with different values.

Chainlink is a type of oracle. The code for reading Data Feeds is the same across all EVM-compatible blockchains and Data Feed types. You choose different types of feeds for different uses, but the request and response formats are the same.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
//1
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceConsumerV3 {
    //2
    AggregatorV3Interface internal priceFeed;

    /**
     * Network: Sepolia
     * Aggregator: BTC/USD
     * Address: 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
     */
    constructor() {
        priceFeed = AggregatorV3Interface(
            0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
        );
    }

    /**
     * Returns the latest price.
     */
    //3
    function getLatestPrice() public view returns (int) {
        // prettier-ignore
        (
            /* uint80 roundID */,
            int price,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = priceFeed.latestRoundData();
        return price;
    }
}

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
The above line imports an interface, the interface is a collection of function headers, without the underlying code, so you know what to use to interact without having to worry about the code.

AggregatorV3Interface internal priceFeed; This line assigns all the interfaces to a single variable called priceFeed.

priceFeed = AggregatorV3Interface(0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43); before starting to use the pricefeed, you have to tell what network you want to run this pricefeed on and what aggregator you want to watch, e.g: BTC/USD. we pass the address of that given contract in AggregatorV3Interface in the constructor.
This creates an object that has all methods associated with it for a given network.

(/* uint80 roundID */, int price, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/) = priceFeed.latestRoundData();
return price;

So it's pretty easy to get what you are looking for by calling the function. The returned price is an integer, so it is missing its decimal point.

Understanding the entire cycle of generating randomness.

In this section, you will learn about generating randomness on blockchains. This includes learning how to implement a Request and Receive cycle with Chainlink oracles and how to consume random numbers with Chainlink VRF in smart contracts.

Randomness is very difficult to generate on blockchains. This is because every node on the blockchain must come to the same conclusion and form a consensus. Even though random numbers are versatile and useful in a variety of blockchain applications, they cannot be generated natively in smart contracts. The solution to this issue is Chainlink VRF, also known as Chainlink Verifiable Random Function.

Randomness, cannot be reference data. If the result of randomness is stored on-chain, any actor could retrieve the value and predict the outcome. Instead, randomness must be requested from an oracle, which generates a number and cryptographic proof. Then, the oracle returns that result to the contract that requested it. This sequence is known as the Request and Receive cycle.

The funding your application requires is managed in a single location.

Create and fund a subscription

before we move ahead with generating random numbers, just read through the following, and get a base Idea. I'll explain the following in detail as we move forward

In Chainlink, the VRFConsumerBaseV2 and VRFCoordinatorV2Interface are two important components related to Verifiable Random Function (VRF) functionality. Let's understand each of them separately:

  1. VRFConsumerBaseV2: VRFConsumerBaseV2 is a contract template provided by Chainlink that acts as a base contract for contracts wanting to consume random numbers generated by Chainlink's VRF system. This template helps in integrating VRF functionality into smart contracts on the Ethereum blockchain.

The VRFConsumerBaseV2 contract provides a set of functions and variables that simplify the process of making VRF requests and handling the response. Here are the key features of VRFConsumerBaseV2:

  • requestRandomness: This function allows the contract to request a random number from the VRFCoordinator contract. It takes the necessary parameters such as the keyHash (a unique identifier for the VRF service), fee (the payment required for the VRF service), and a user-specific seed.

  • fulfillRandomness: This function is called by the VRFCoordinator contract once the random number has been generated. It receives the requestId and the random number as parameters. Contracts inheriting from VRFConsumerBaseV2 must implement this function to handle the generated random number.

  • rawFulfillRandomness: This function is similar to fulfillRandomness but accepts the response as raw bytes instead of a uint256. It can be used when the random number is more complex and needs to be parsed within the contract.

  • randomness: A variable that stores the generated random number.

The VRFConsumerBaseV2 contract provides a convenient and standardized way for smart contracts to interact with Chainlink's VRF system and use random numbers within their applications.

  1. VRFCoordinatorV2Interface: VRFCoordinatorV2Interface is an interface contract that defines the functions required to interact with Chainlink's VRFCoordinator contract. The VRFCoordinator contract acts as the coordinator and manager of VRF requests and responses.

The VRFCoordinatorV2Interface defines the following functions:

  • requestRandomness: This function is used to request a random number. It takes the parameters required for the request, such as the keyHash, fee, and seed. The VRFCoordinator contract processes this request and initiates the VRF generation process.

  • randomnessRequestRejected: This function is called by the VRFCoordinator contract when a VRF request is rejected. It provides the requestId and an error message as parameters.

  • randomnessRequestFulfilled: This function is called by the VRFCoordinator contract when a VRF request is fulfilled and the random number is generated. It provides the requestId and the generated random number as parameters.

The VRFCoordinatorV2Interface defines the required functions for contracts to interact with the VRFCoordinator contract and receive the generated random numbers.

Both the VRFConsumerBaseV2 contract and VRFCoordinatorV2Interface interface play important roles in integrating Chainlink's VRF functionality into smart contracts, allowing them to securely generate and use verifiable random numbers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract VRFD20 is VRFConsumerBaseV2 {

}

Contract variables

  • uint64 s_subscriptionId: The subscription ID is then used as a reference to track the progress and status of the VRF request.
    It helps the VRF Coordinator contract to associate the generated random number with the correct requestor, ensuring that the random number is delivered to the appropriate contract.

    The subscription ID provides a way to differentiate between different VRF requests and allows the VRF system to process them independently.

  • address vrfCoordinator: The address of the Chainlink VRF Coordinator contract.

  • bytes32 s_keyHash: It tells the VRF system which service to use. This helps maintain security and ensures that the random numbers generated come from the intended VRF service.

in the above image, we can see the key hashes for the max gwei we are willing to pay per request.

  • uint32 callbackGasLimit : The limit for how much gas to use for the callback request to your contract's fulfillRandomWords function. It must be less than the maxGasLimit on the coordinator contract. Adjust this value for larger requests depending on how your fulfillRandomWords function processes and stores the received random values. If your callbackGasLimit is not sufficient, the callback will fail and your subscription is still charged for the work done to generate your requested random values.

  • uint16 requestConfirmations : How many confirmations the Chainlink node should wait before responding. The longer the node waits, the more secure the random value is

  • uint32 numWords : How many random values to request.

  •   mapping(uint256 => address) private s_rollers;
      mapping(address => uint256) private s_results;
    

    - s_rollers: stores mapping between requestId and the address of who made that call. the requestId is recieved when request to the chainlink random function is made.

    - s_results: stores the result for a given address

  • Outline of how the variables will be set

      uint64 s_subscriptionId;
      address vrfCoordinator = 0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D;
      bytes32 s_keyHash = 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15;
      uint32 callbackGasLimit = 40000;
      uint16 requestConfirmations = 3;
      uint32 numWords =  1;
    

Constructor:

    constructor(uint64 subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_owner = msg.sender;
        s_subscriptionId = subscriptionId;
    }

based on which chain we run on, we can get the vrfCoordinator.
url: https://docs.chain.link/vrf/v2/subscription/supported-networks

Now we are going to deal with 3 functions that make up this project from the documentation, i.e
* rollDice
* fulfillRandomWords

rollDice

// rollDice function
function rollDice() public onlyOwner returns (uint256 requestId) {
    //checks if player has already rolled the dice.
    require(s_results[msg.sender] == 0, "Already rolled");
    // Will revert if subscription is not set and funded.
    requestId = COORDINATOR.requestRandomWords(
//The unique identifier (hash) of the VRF service provider.
        s_keyHash, 
//The unique identifier of the subscription for VRF randomness.
        s_subscriptionId,
//The number of required confirmations for the VRF request.
        requestConfirmations,
//The gas limit for the VRF callback function.
        callbackGasLimit,
//the number of words    
        numWords
    );
    //the requestId holds a value that points to a given placeholder that the value will be saved in.
    s_rollers[requestId] = msg.sender;
    s_results[msg.sender] = ROLL_IN_PROGRESS;
    emit DiceRolled(requestId, msg.sender);
}

we'll get the following values by following the given links

link: https://docs.chain.link/vrf/v2/subscription/supported-networks

fulfillRandomWords

It is an inbuilt function that returns an array of random values, you'll have to override it to fit your requirements

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {

        // transform the result to a number between 1 and 20 inclusively
        uint256 d20Value = (randomWords[0] % 20) + 1;

        // assign the transformed value to the address in the s_results mapping variable
        s_results[s_rollers[requestId]] = d20Value;

        // emitting event to signal that dice landed
        emit DiceLanded(requestId, d20Value);
    }

All in short after watching the below video you'll understand the following flow, all you have to do is create a subscription account and add LINK tokens there, after that you can deploy as many contracts as you want using this subscription account.
Just don't forget to link those contracts back to your account.

MUST WATCH TO UNDERSTAND BETTER
YOUTUBE: https://www.youtube.com/watch?v=rdJ5d8j1RCg

subscription_manager: https://vrf.chain.link/?_ga=2.197108143.268824100.1684125657-241396006.1682660284&_gac=1.124302584.1683028767.Cj0KCQjw6cKiBhD5ARIsAKXUdyYPTNXSnoSqyfK9AiJD_2AfTNF1YH7vNoZUMRjv8T-5Vivv_vI_gHsaAojLEALw_wcB

API Calls: Using Any API

In this section you will learn how to request data from a public API in a smart contract. This includes understanding what Tasks and External adapters are and how Oracle Jobs uses them. You will also learn how to find the Oracle Jobs and Tasks for your contract and how to request data from an Oracle Job.

Before we move forward we need to understand Basic Request Model.

Basic Request Model

ChainlinkClient is a parent contract that enables smart contracts to consume data from oracles.

The client constructs and requests a known Chainlink oracle through the transferAndCall function. This request contains encoded information that is required for the cycle to succeed. In the ChainlinkClient contract, this call is initiated with a call to sendChainlinkRequestTo.

sendChainlinkRequestTo() -> transferCall() -> oracle

to make a request you'll need the following values.

  • Oracle address

  • Job Id

  • callback function which the Oracle sends the response to.

we have a function called fulfillOracleRequest() which basically runs when the Oracle executes its job, the result is forwarded in this method which is then forwarded to the client.

The off-chain oracle node is responsible for listening for events emitted by its corresponding smart contract. Once it detects an OracleRequest event uses the data emitted to perform a job. The most common job type for a Node is to make a GET request to an API.

In the above example when dealing with the random numbers, the response was sent to a function defined by the VRFConsumerBase, which we overwrote to handle the random number.
While dealing with the API, that is not the case, we/"the contract" have to define the function we want the value in.

What are Jobs?

In Chainlink, a "job" refers to a task or a set of instructions that defines the data to be fetched from an external source, how to fetch that data, and how to deliver it to a smart contract on the blockchain.

Here are some key points about jobs in Chainlink:

  1. Data Fetching: Jobs are responsible for fetching data from external APIs, off-chain systems, or other off-chain sources. They specify the parameters, endpoints, or URLs required to retrieve the desired data.

  2. Data Aggregation and Transformation: Jobs can aggregate and transform the fetched data as needed before delivering it to the smart contract. This allows the data to be manipulated or converted into a suitable format for further processing.

  3. Oracles: Jobs interact with oracles, which are nodes in the Chainlink network that execute the job's instructions and provide the requested data to the smart contract. Oracles act as trusted intermediaries between the off-chain world and the on-chain smart contract.

  4. Data Delivery: Once the data is fetched and processed, the job delivers the data to the specified smart contract on the blockchain. This ensures that the smart contract has access to the required data from external sources.

  5. Job Specifications: Jobs are defined using job specifications, which are written in JSON format. These specifications outline the parameters, adapters (data source adapters and job pipeline adapters), and other details necessary to execute the job.

  6. Job Pipelines: Jobs can also include job pipelines, which allow for multiple steps of data processing and manipulation. Job pipelines consist of adapters that transform, filter, or perform operations on the data before delivering it to the smart contract.

By defining jobs and their specifications, Chainlink enables smart contracts to securely and reliably access external data and utilize it in their on-chain operations. This facilitates the integration of real-world data and services into decentralized applications and enables smart contracts to make informed decisions based on the retrieved data

What are tasks?

A "task" refers to a specific step or operation within a job. Tasks are used to define the individual actions or transformations that need to be performed on data during the data-fetching process.

If a job needs to make a GET request to an API, find a specific unsigned integer field in a JSON response, and then submit that back to the requesting contract, it would need a job containing the following Tasks:

  • HTTP calls the API. The method must be set to GET.

  • JSON Parse parses the JSON and extracts a value at a given keypath. eg: walks a specified path ("RAW,ETH,USD,VOLUME24HOUR") and returns the value found at that result. Example: 703946.0675653099

  • Multiply multiplies the input by a multiplier. Used to remove the decimals. eg: parses the input into a float and multiplies it by the 10^18. Example: 703946067565309900000000
  • ETH ABI Encode converts the data to a bytes payload according to ETH ABI encoding.

  • ETH Tx submits the transaction to the chain, completing the cycle.

Now that we've understood the basics of calling an API. let's understand it with the help of a contract.

Understanding the Contract

before we move on to the contract, I'd like to explain a few points regarding the contract, so you'll understand the contract better when going through it.

import @chainlink/contracts/src/v0.8/ChainlinkClient.sol

ChainlinkClient.sol file provides functionality for interacting with the Chainlink Oracle network

contract APIConsumer is ChainlinkClient

The above line inherits ChainlinkClient into our contract.

using Chainlink for Chainlink.Request;

The above line means all functions of Chainlink should be used by the data structure Chainlink.Request. which is a struct created within ChainlinkClient.sol file.
That means you can access the functions via the datatype called Chainlink.Request.

setChainlinkToken(0x779877A7B0D9E8603169DdbD7836e478b4624789);

This line calls the setChainlinkToken function, which is a function provided by the Chainlink library. It sets the Chainlink token address to 0x779877A7B0D9E8603169DdbD7836e478b4624789. This address represents the specific Chainlink token used within the contract.

setChainlinkOracle(0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD);

This address represents the specific Chainlink oracle used within the contract.

    constructor() ConfirmedOwner(msg.sender) {
        setChainlinkToken(0x779877A7B0D9E8603169DdbD7836e478b4624789);
        setChainlinkOracle(0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD); //! based on the network that supports it
        jobId = "ca98366cc7314957b8c012c72f05aeeb"; //! based on the network that supports it
        fee = (1 * LINK_DIVISIBILITY) / 10; // 0,1 * 10**18 (Varies by network and job)
    }

code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";

/**
 * Request testnet LINK and ETH here: https://faucets.chain.link/
 * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
 */

/**
 * THIS IS AN EXAMPLE CONTRACT WHICH USES HARDCODED VALUES FOR CLARITY.
 * THIS EXAMPLE USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

contract APIConsumer is ChainlinkClient, ConfirmedOwner {
    using Chainlink for Chainlink.Request;

    uint256 public volume;
    bytes32 private jobId; 
    uint256 private fee;

    event RequestVolume(bytes32 indexed requestId, uint256 volume);

    /**
     * @notice Initialize the link token and target oracle
     *
     * Sepolia Testnet details:
     * Link Token: 0x779877A7B0D9E8603169DdbD7836e478b4624789
     * Oracle: 0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD (Chainlink DevRel)
     * jobId: ca98366cc7314957b8c012c72f05aeeb
     *
     */
    constructor() ConfirmedOwner(msg.sender) {
        setChainlinkToken(0x779877A7B0D9E8603169DdbD7836e478b4624789);  
//we got this from "https://docs.chain.link/resources/link-token-contracts"
        setChainlinkOracle(0x6090149792dAAeE9D1D568c9f9a6F6B46AA29eFD); 
//we got this from the market place, based on which oracle you use
        jobId = "ca98366cc7314957b8c012c72f05aeeb"; 
//we got this from the market place
        fee = 11 * 10 ** 17 // 1.1 LINK 
//LINK_DIVISIBILITY is 10**18, its mentioned in the imported contract
// 0,1 * 10**18 (Varies by network and job)
    }

    /**
     * Create a Chainlink request to retrieve API response, find the target
     * data, then multiply by 1000000000000000000 (to remove decimal places from data).
     */
    function requestVolumeData() public returns (bytes32 requestId) {
        Chainlink.Request memory req = buildChainlinkRequest(
            jobId,
            address(this), //callback address
            this.fulfill.selector //the fullment method, this smartcontract has a method called fulfill
        );

        // Set the URL to perform the GET request on
        //the following are adapters, it means its like a code that runs to do your job
        req.add(
            "get",
            "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD"
        );

        // Set the path to find the desired data in the API response, where the response format is:
        // {"RAW":
        //   {"ETH":
        //    {"USD":
        //     {
        //      "VOLUME24HOUR": xxx.xxx,
        //     }
        //    }
        //   }
        //  }
        // request.add("path", "RAW.ETH.USD.VOLUME24HOUR"); // Chainlink nodes prior to 1.0.0 support this format
        req.add("path", "RAW,ETH,USD,VOLUME24HOUR"); // Chainlink nodes 1.0.0 and later support this format

        // Multiply the result by 1000000000000000000 to remove decimals
        int256 timesAmount = 10 ** 18; //incase the value is in decimals, we have to add this so its a integer, and not a decimal

        req.addInt("times", timesAmount); //you dont have to pass the datatype

        // Sends the request
        return sendChainlinkRequest(req, fee); //the fee will be mentioned in the marketplace
    }

    /**
     * Receive the response in the form of uint256
     */
    function fulfill(
        bytes32 _requestId, //requestId
        uint256 _volume //data
    ) public recordChainlinkFulfillment(_requestId) {
        emit RequestVolume(_requestId, _volume);
        volume = _volume;
    }

    /**
     * Allow withdraw of Link tokens from the contract
     */
    function withdrawLink() public onlyOwner {
        LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
        require(
            link.transfer(msg.sender, link.balanceOf(address(this))),
            "Unable to transfer"
        );
    }
}

you gotta send link tokens to the contract after deploying it so it can be used during accessing API data from oracles.

The setChainlinkToken function sets the LINK token address for the network you are deploying to. The setChainlinkOracle function sets a specific Chainlink oracle that a contract makes an API call from. The jobId refers to a specific job for that node to run.

Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/

Each job is unique and returns different types of data. For example, a job that returns a bytes32 variable from an API would have a different jobId than a job that retrieved the same data, but in the form of a uint256 variable.

that's about it :)
thankyou for reading :P

0
Subscribe to my newsletter

Read articles from Aaron Rebelo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aaron Rebelo
Aaron Rebelo

Hi, I'm Aaron Rebelo, a MERN stack developer who's passionate about creating robust and engaging web applications. My expertise lies in using MongoDB, Express, React, and Node.js to build applications that are not only functional but also visually appealing. In addition to my work as a developer, I'm currently learning Solidity, a programming language used for developing smart contracts on the Ethereum blockchain. I believe that blockchain technology is the future, and I'm excited to be a part of this emerging industry. I'm also interested in digital marketing and believe that effective marketing strategies are key to the success of any project. That's why I'm committed to learning the latest marketing techniques and strategies to help my clients achieve their goals. In my spare time, I've started a video series on my YouTube channel, @thedecadehypothesis, where I document my progress on building new habits every 10 days. I believe that building new habits is essential for personal growth, and I'm excited to share my journey with others. Thank you for taking the time to learn more about me. I'm passionate about my work and excited about the opportunities that lie ahead. Please feel free to connect with me if you have any questions or if you're interested in working together.