Blockchain Masterclass for JavaScript Developers
  • 👋Welcome
  • 🐣Hello World
  • 📰Decentralized News Service Contract
  • 🖥️Building A Frontend
  • 📖Glossary
Powered by GitBook
On this page
  • What are we building?
  • How will we build it?
  • Create The Smart Contract
  • Adding Our News Source
  • Creating The Functions Subscription
  • Deploy Time

Decentralized News Service Contract

PreviousHello WorldNextBuilding A Frontend

Last updated 1 year ago

All of the code for this project can be found on

You made it! You've deployed a smart contract! We will take one MASSIVE step forward and build an entire blockchain application. What are we going to be making? A decentralized, censorship-resistant news headline aggregator based on hacker news. This is commonly referred to as a dApp or Distributed Application

What are we building?

A decentralized, censorship-resistant news headline aggregator based on hacker news. That's a lot of buzzwords. Let's break it down. This application will use to call the hacker news API. It'll return the latest news story and store the title and link in a smart contract. This will give us several advantages.

  1. Distributed back-end

    We will be using a blockchain to store the data, which will provide a fault-tolerant distributed back-end. This can be compared to a database that is shared across independent providers.

  2. Censorship Resistance

    Due to the decentralized nature of the blockchain, the data will be available as long as at least one node in the network is up and running, making it resistant to censorship.

  3. Immutable

    Smart contracts are immutable, providing transparency and accountability as all data modifications are visible on the blockchain.

How will we build it?

Create The Smart Contract

Below is a starting point for a basic Chainlink Functions Consumer smart contract.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol";

contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner {
    using FunctionsRequest for FunctionsRequest.Request;

    bytes32 public s_lastRequestId;
    bytes public s_lastResponse;
    bytes public s_lastError;

    error UnexpectedRequestID(bytes32 requestId);

    event Response(bytes32 indexed requestId, bytes response, bytes err);
   
    // CUSTOM PARAMS - START
    //Sepolia Router address;
    // Additional Routers can be found at 
    //   https://docs.chain.link/chainlink-functions/supported-networks
    address router = 0xb83E47C2bC239B3bf370bc41e1459A34b41238D0; 
    
    //Functions Subscription ID
    uint64 subscriptionId = 9999; // Fill this in with your subscription ID
    
    //Gas limit for callback tx do not change
    uint32 gasLimit = 300000;
    
    //DoN ID for Sepolia, from supported networks in the docs
    // Additional DoN IDs can be found at 
    //   https://docs.chain.link/chainlink-functions/supported-networks
    bytes32 donId = 0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000; 

    // Source JavaScript to run
    string source = "";
    
    // CUSTOM PARAMS - END

    constructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}

    function sendRequest() external onlyOwner returns (bytes32 requestId) {

        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(source);
        
        s_lastRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            gasLimit,
            donId
        );
        
        return s_lastRequestId;
    }

    function fulfillRequest(
        bytes32 requestId,
        bytes memory response,
        bytes memory err
    ) internal override {
        if (s_lastRequestId != requestId) {
            revert UnexpectedRequestID(requestId);
        }
        
        s_lastResponse = response;

        s_lastError = err;
        emit Response(requestId, s_lastResponse, s_lastError);
    }
}

Adding Our News Source

We'll modify this contract to house our news articles and pass the correct call to Chainlink Functions.

First, let's add a struct and array to hold our articles. A struct is a custom-defined type that allows us to group variables. After bytes public s_lastError; add

    struct Article {
        bytes url;
        uint256 publishedDate;
    }

    Article[] private articles;

This will create a place for our articles to live and record the date we added them.

We can also add an event after event Response to keep a log of all the articles we add.

    event ArticleAdded(bytes url, uint256 publishedDate);

Once we've added the event, the next step is to add our source JavaScript.

This JavaScript will be need to:

  • Fetch latest news stories from HackerNews

  • Filter out the latest story

  • Fetch the details of that story and story the title and URL

  • Return the story title and URL

The code we'll need looks something like this

const url = `https://hacker-news.firebaseio.com/v0/newstories.json`;
const newRequest = Functions.makeHttpRequest({ url });
const newResponse = await newRequest;

if (newResponse.error) {
  throw Error(`Error fetching news`);
}

// HN can sometimes return story objects without a `url` property. So retry.
let itemIdx = 0
let done = false
let storyUrl;
while (!done) {
  const latestStory = newResponse.data[itemIdx];
  const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`;
  const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL });
  const storyResponse = await storyRequest;


  if (!storyResponse.data.url) {
    console.log("\nReturned  object missing URL property. Retrying...")
    itemIdx += 1
    continue
  }
  
  storyUrl = storyResponse.data.url
  done = true;
}

return Functions.encodeString(storyUrl);

One thing to note is you MUST have a ; after each statement. This is because we will store this script as a single line in our contract.

This JavaScript will be stored in the source variable in our contract. Replace the source variable line in the smart contract with the following which includes the entire JavaScript compressed into a single line with all semi colons!

const url = `https://hacker-news.firebaseio.com/v0/newstories.json`; const newRequest = Functions.makeHttpRequest({ url }); const newResponse = await newRequest; if (newResponse.error) { throw Error(`Error fetching news`);} let itemIdx = 0; let done = false; let storyUrl; while (!done) { const latestStory = newResponse.data[itemIdx];const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`; const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL }); const storyResponse = await storyRequest; if (!storyResponse.data.url) {console.log(`\nReturned  object missing URL property. Retrying...`); itemIdx += 1; continue;} storyUrl = storyResponse.data.url;done = true;}return Functions.encodeString(storyUrl);

This single line script will be stored in the source variable in our contract.

string source =
        // handles errors where HN returns an object without a URL property.
        "const url = `https://hacker-news.firebaseio.com/v0/newstories.json`; const newRequest = Functions.makeHttpRequest({ url }); const newResponse = await newRequest; if (newResponse.error) { throw Error(`Error fetching news`);} let itemIdx = 0; let done = false; let storyUrl; while (!done) { const latestStory = newResponse.data[itemIdx];const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`; const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL }); const storyResponse = await storyRequest; if (!storyResponse.data.url) {console.log(`\nReturned  object missing URL property. Retrying...`); itemIdx += 1; continue;} storyUrl = storyResponse.data.url;done = true;}return Functions.encodeString(storyUrl);";

We will need to update subscriptionId. We'll come back to that later.

Within fulfillRequest(), we need to add logic to decode the response from our JavaScript, push the article into our articles array, and emit the ArticleAdded event. We'll add this code after the if checking s_lastRequestId

        articles.push(Article(response, block.timestamp));
        emit ArticleAdded(response, block.timestamp);

The last addition to our smart contract will be adding getAllArticles(), allowing us to see all of the articles we have saved so far.

    // Function to return all articles
    function getAllArticles() public view returns (string[] memory) {
        string[] memory allArticles = new string[](articles.length);
        for (uint i = 0; i < articles.length; i++) {
            allArticles[i] = string(articles[i].url);
        }
        return allArticles;
    }

Creating The Functions Subscription

To process our request, we'll need to create a Functions Subscription.

  1. Connect your wallet and make sure you are on the Ethereum Sepolia test network.

  2. Click 'Create Subscription'

  3. Fill in your email address and select the checkbox for the Privacy Policy

  4. Approve subscription creation

  5. Sign the message to accept the Terms of Service

  6. Select add funds

  7. Add funds to your subscription. I'd suggest 10 LINK

  8. Confirm the transaction

  9. Deploy your contract with the subscription changed, see below

  10. Add a consumer

At this point you have your subscription Id you'll need to add that to the smart contract before deploying it. Replace 999 with your value here

uint64 subscriptionId = 999;

The final code should look like this with the subscriptionId changed.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol";

contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner {
    using FunctionsRequest for FunctionsRequest.Request;

    bytes32 public s_lastRequestId;
    bytes public s_lastResponse;
    bytes public s_lastError;

    error UnexpectedRequestID(bytes32 requestId);

    struct Article {
        bytes url;
        uint256 publishedDate;
    }

    Article[] private articles;

    event Response(bytes32 indexed requestId, bytes response, bytes err);
    event ArticleAdded(bytes url, uint256 publishedDate);

    // CUSTOM PARAMS - START
    //Sepolia Router address;
    // Additional Routers can be found at
    //   https://docs.chain.link/chainlink-functions/supported-networks
    address router = 0xb83E47C2bC239B3bf370bc41e1459A34b41238D0;

    //Functions Subscription ID
    uint64 subscriptionId = 1796; // Fill this in with your subscription ID

    //Gas limit for callback tx do not change
    uint32 gasLimit = 300000;

    //DoN ID for Sepolia, from supported networks in the docs
    // Additional DoN IDs can be found at
    //   https://docs.chain.link/chainlink-functions/supported-networks
    bytes32 donId =
        0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000;

    // Source JavaScript to run
    string source =
        "const url = `https://hacker-news.firebaseio.com/v0/newstories.json`; const newRequest = Functions.makeHttpRequest({ url }); const newResponse = await newRequest; if (newResponse.error) { throw Error(`Error fetching news`);} let itemIdx = 0; let done = false; let storyUrl; while (!done) { const latestStory = newResponse.data[itemIdx];const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`; const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL }); const storyResponse = await storyRequest; if (!storyResponse.data.url) {console.log(`\nReturned  object missing URL property. Retrying...`); itemIdx += 1; continue;} storyUrl = storyResponse.data.url;done = true;}return Functions.encodeString(storyUrl);";
    // CUSTOM PARAMS - END

    constructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}

    function sendRequest() external onlyOwner returns (bytes32 requestId) {
        FunctionsRequest.Request memory req;
        req.initializeRequestForInlineJavaScript(source);

        s_lastRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            gasLimit,
            donId
        );

        return s_lastRequestId;
    }

    function fulfillRequest(
        bytes32 requestId,
        bytes memory response,
        bytes memory err
    ) internal override {
        if (s_lastRequestId != requestId) {
            revert UnexpectedRequestID(requestId);
        }
        articles.push(Article(response, block.timestamp));
        emit ArticleAdded(response, block.timestamp);

        s_lastResponse = response;

        s_lastError = err;
        emit Response(requestId, s_lastResponse, s_lastError);
    }

    // Function to return all articles
    function getAllArticles() public view returns (string[] memory) {
        string[] memory allArticles = new string[](articles.length);
        for (uint i = 0; i < articles.length; i++) {
            allArticles[i] = string(articles[i].url);
        }
        return allArticles;
    }
}

Deploy Time

Alright, we have our contract ready to go! Now you'll need to deploy it to Sepolia. This should follow the same process we used for deploying the Hello World contract.

All of the code for this project can be found on

Head to to create this smart contract. The interface should look familiar. You can copy and paste this code directly into a new file in remix. You may encounter a message about pasted code. It should be okay to accept it and continue.

If you want to try out this code, head to . There, you can test the code and see the results.

Head to

Once your contract is deployed, you'll need to head back to and add the deployed contract as a consumer.

You're now ready to call sendRequest() from remix to initiate a Function request. Once the request is complete getAllArticles() should return the latest headline from HackerNews. Congratulations! You're successfully using Chainlink Functions to store data from an API on-chain!

📰
GitHub
https://remix.ethereum.org/
https://functions.chain.link/playground
https://functions.chain.link
https://functions.chain.link
GitHub
Chainlink Functions
Remix - Ethereum IDE
Functions | Chainlinkchainlink
Logo
Astro
Logo
Logo