All of the code for this project can be found on GitHub
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 Chainlink Functions 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.
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.
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.
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
All of the code for this project can be found on GitHub
Below is a starting point for a basic Chainlink Functions Consumer smart contract.
// SPDX-License-Identifier: MITpragmasolidity 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";contractFunctionsConsumerExampleisFunctionsClient, ConfirmedOwner {usingFunctionsRequestforFunctionsRequest.Request;bytes32public s_lastRequestId;bytespublic s_lastResponse;bytespublic s_lastError;errorUnexpectedRequestID(bytes32 requestId);eventResponse(bytes32indexed requestId, bytes response, bytes err);// CUSTOM PARAMS - START//Sepolia Router address;// Additional Routers can be found at // https://docs.chain.link/chainlink-functions/supported-networksaddress router =0xb83E47C2bC239B3bf370bc41e1459A34b41238D0; //Functions Subscription IDuint64 subscriptionId =9999; // Fill this in with your subscription ID//Gas limit for callback tx do not changeuint32 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-networksbytes32 donId =0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000; // Source JavaScript to runstring source ="";// CUSTOM PARAMS - ENDconstructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}functionsendRequest() externalonlyOwnerreturns (bytes32 requestId) { FunctionsRequest.Request memory req; req.initializeRequestForInlineJavaScript(source); s_lastRequestId =_sendRequest( req.encodeCBOR(), subscriptionId, gasLimit, donId );return s_lastRequestId; }functionfulfillRequest(bytes32 requestId,bytesmemory response,bytesmemory err ) internaloverride {if (s_lastRequestId != requestId) {revertUnexpectedRequestID(requestId); } s_lastResponse = response; s_lastError = err;emitResponse(requestId, s_lastResponse, s_lastError); }}
Head to https://remix.ethereum.org/ 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.
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
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!
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
Connect your wallet and make sure you are on the Ethereum Sepolia test network.
Click 'Create Subscription'
Fill in your email address and select the checkbox for the Privacy Policy
Approve subscription creation
Sign the message to accept the Terms of Service
Select add funds
Add funds to your subscription. I'd suggest 10 LINK
Confirm the transaction
Deploy your contract with the subscription changed, see below
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: MITpragmasolidity 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";contractFunctionsConsumerExampleisFunctionsClient, ConfirmedOwner {usingFunctionsRequestforFunctionsRequest.Request;bytes32public s_lastRequestId;bytespublic s_lastResponse;bytespublic s_lastError;errorUnexpectedRequestID(bytes32 requestId);structArticle {bytes url;uint256 publishedDate; } Article[] private articles;eventResponse(bytes32indexed requestId, bytes response, bytes err);eventArticleAdded(bytes url, uint256 publishedDate);// CUSTOM PARAMS - START//Sepolia Router address;// Additional Routers can be found at// https://docs.chain.link/chainlink-functions/supported-networksaddress router =0xb83E47C2bC239B3bf370bc41e1459A34b41238D0;//Functions Subscription IDuint64 subscriptionId =1796; // Fill this in with your subscription ID//Gas limit for callback tx do not changeuint32 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-networksbytes32 donId =0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000;// Source JavaScript to runstring 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 - ENDconstructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}functionsendRequest() externalonlyOwnerreturns (bytes32 requestId) { FunctionsRequest.Request memory req; req.initializeRequestForInlineJavaScript(source); s_lastRequestId =_sendRequest( req.encodeCBOR(), subscriptionId, gasLimit, donId );return s_lastRequestId; }functionfulfillRequest(bytes32 requestId,bytesmemory response,bytesmemory err ) internaloverride {if (s_lastRequestId != requestId) {revertUnexpectedRequestID(requestId); } articles.push(Article(response, block.timestamp));emitArticleAdded(response, block.timestamp); s_lastResponse = response; s_lastError = err;emitResponse(requestId, s_lastResponse, s_lastError); }// Function to return all articlesfunctiongetAllArticles() publicviewreturns (string[] memory) {string[] memory allArticles =newstring[](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.
Once your contract is deployed, you'll need to head back to https://functions.chain.link 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!