Create a dApp To Pay Recording Artists Based on Their Spotify Streams
Web3 is new Internet infrastructure that uses decentralized and distributed networks to enable peer-to-peer interactions without the need for intermediaries. It is underpinned by blockchain technology, which is a shared ledger that provides high levels of security, transparency, and immutability.
Blockchain technology provides many benefits through its highly secure design, but blockchains have a notable functional limitation commonly referred to as the “blockchain oracle problem.” Essentially, blockchain networks are inherently closed systems that cannot natively access real-world data or computation. The oracle problem has significantly limited the types of applications blockchain developers can create, since most advanced use cases require some form of real-world data or computation.
Chainlink was created to solve the oracle problem and connect all of the world’s data and systems to blockchains. Chainlink’s highly secure data and computation capabilities have fueled the growth of “DeFi” or decentralized finance (enabling over $7T in transaction value since the start of 2022), gaming, NFTs, and many other Web3 verticals.
In this tutorial, we’ll show you how to use Chainlink Functions—a decentralized serverless computing platform that can help you connect the world’s APIs to blockchains. You will learn how to build a Web3 decentralized application (or dApp for short) that acts as a digital agreement between a record label and a music artist.
Smart Contracts for Musical Artists
The logic for the digital agreement defines how much an artist should be paid per music stream, and is stored in a smart contract deployed on the blockchain.
A “smart contract” is a simple program that contains logic to determine what output it should give—including transferring value—executed across hundreds or thousands of nodes on a blockchain, providing extremely high levels of security, and guaranteed execution.
When this program is executed, it will use Chainlink Functions to call an API to get the number of streams for the artist, as well as interact with the Twilio SendGrid Email API to send an email to the artist about their payment. In this case, Chainlink Functions acts as a middleware layer between the program deployed on the blockchain and the external Web, and carries out your computational “heavy lifting” in a cost-effective manner without any loss of the cryptographic guarantees that the blockchain offers.
Prerequisites
Before you begin, you’ll need to set up a few free accounts and install some software if you haven’t yet:
- Sign up to Chainlink Functions beta.
- A free Twilio Sendgrid account.
- A sender email address that you control, verified by Twilio (note: It’s best to use a public email service provider like Gmail or Hotmail, and this step could take more than a day).
- Node.js v16+, NPM, and Git.
- A Web3 wallet set up in your browser, such as MetaMask.
- Free RPC URLs for blockchain access through Infura or Alchemy.
Sometimes these steps can take a day or two, so don’t get frustrated—just bookmark this blog.
When you’re set-up, let’s get started!
Set Up Your Dev Environment
We can start by cloning the Chainlink Functions Sample Repository to your development machine. This is a pre-packaged code repository that contains all the code needed to deploy and execute this example. You should take a few minutes to read the README as well. Once cloned, use the terminal to `cd` into your project directory and install all the dependencies with:
npm install
You also want to set up your Web3 wallet (for example, Metamask) and ensure it’s funded with the right tokens—in this case you’ll need some ETH and LINK. You need to use this wallet account and the tokens in it to pay for broadcasting and submitting transactions to the blockchain network. You can think of it like a travel pass that you need to keep funded to access public transport.
You can use Ethereum’s Sepolia test network, the Polygon Mumbai test network, or the Avalanche Fuji test network to deploy this code. These are copies of the production networks that developers use to build and test applications. In this post, we’ll use the Ethereum Sepolia network.
You will also need to get access to the Sepolia network by adding it to your MetaMask Web3 wallet. The easiest way to do this is to head to the Chainlist website and pressing the “Add to MetaMask” button:
Alternatively, you can sign up for your own free API key via an external RPC provider such as Infura or Alchemy, then manually add the network to your MetaMask wallet. You will also need these keys when configuring the environment variables in your code.
The next step, as mentioned above, is to make sure you’ve funded your wallet with Sepolia ETH and LINK. The ETH is required as “gas” to broadcast and submit transactions to the network. LINK is required to pay the Chainlink oracle network for servicing your requests to reach out to APIs. You can add the LINK token to your MetaMask wallet’s assets list by going here and clicking “Add to Wallet”.
Now that you’re all set in terms of connection to the network and funds to do things on it, we can move on to the code!
Set Up Your Environment Variables
You will need to use environment variables to store API secrets and other sensitive information outside of your repository’s codebase.
The env vars you need for this sample application are:
MUMBAI_RPC_URL // Get from Alchemy or Infura as part of Setup steps GITHUB_API_TOKEN= // see next instruction for how to get this. #AND PRIVATE_KEY // get this from Metamask as instructed in Setup SECOND_PRIVATE_KEY // get this from Metamask as instructed in Setup #AND # Spotify wrapper API keys [see: https://doc.api.soundcharts.com/api/v2/doc] SOUNDCHART_APP_ID="soundcharts" SOUNDCHART_API_KEY="soundcharts" # EMAIL SERVICE TWILIO_API_KEY // From Twilio/Sengrid API docs ARTIST_EMAIL // an email address you can check emails at VERIFIED_SENDER // your email address that is verified by Twilio. Use gmail, yahoo etc as protonmail often has access problems.
For security best practices, we discourage the use of .env files. Instead, we will use the env-enc package that we shipped with the repo to encrypt all your keys at rest, on your machine, with a password that you provide.
The Twilio API key can be obtained from the settings once you’re logged into your SendGrid account. Your wallet private keys (you need two for this example!) can be extracted from your MetaMask Web3 wallet. The Soundchart variables can be obtained from the Soundcharts API documentation. Note for this sample app we will use the sandboxed API data, which is a snapshot and not hooked to real-time data.
To encrypt your environment variables, you will need to do the following:
- Set up your GitHub private gist access token.
- Configure the
env-enc
package with a password it can use to encrypt your env vars. - Set each env var in the tool so that the encrypted env vars are stored in your project root in the
.env.enc
file that is not human readable.
You can achieve all of the above by following the instructions in the README under the Setup & Environment Variables section.
Set Up the Sample App
The Recording Artist contract example app uses CLI tooling built on top of the Hardhat CLI.
If you’re wondering how the Hardhat CLI tooling knows what data to pass to our smart contract, that is found in the ./Functions-request-config.js file.
The requestConfig
object contains all the data needed for executeRequest()
in the smart contract RecordLabel.sol and also other data necessary for the CLI tooling to process data returned from the decentralized oracle network. Your request configuration object in the config file has the following properties:
js const requestConfig = { // location of source code (only Inline is currently supported) codeLocation: Location.Inline, // location of secrets (Inline or Remote) secretsLocation: Location.Remote, // code language (only JavaScript is currently supported) codeLanguage: CodeLanguage.JavaScript, // string containing the source code to be executed. Relative path used. source: fs.readFileSync("./Twilio-Spotify-Functions-Source-Example.js").toString(), // secrets can be accessed within the source code with secrets.varName (ie: secrets.apiKey) secrets: {}, // ETH wallet key used to sign secrets so they cannot be accessed by a 3rd party walletPrivateKey: process.env["PRIVATE_KEY"], // args (string only array) can be accessed within the source code with args[index] (ie: args[0]). // artistID is the externally supplied Arg. Artist details are stored on contract. // args in sequence are: ArtistID, artistname, lastListenerCount, artist email args: ["ca22091a-3c00-11e9-974f-549f35141000", "Tones&I", "14000000", process.env.ARTIST_EMAIL, process.env.VERIFIED_SENDER], // TONES_AND_I, 14 million // expected type of the returned value expectedReturnType: ReturnType.int256, // Redundant URLs which point to encrypted off-chain secrets. // You *must* generate your own by following instructions in the READ ME for Off-chain secrets secretsURLs: [ "https://gist.githubusercontent.com/zeuslawyer/b307549406ad4c72b741efc5b1547332/raw/b977d4a9493faa17e4469cfdb01e260fec9c5df5/ETH.txt", // "https://gist.githubusercontent.com/zeuslawyer/b307549406ad4c72b741efc5b1547332/raw/b977d4a9493faa17e4469cfdb01e260fec9c5df5/POLY" ], // Default offchain secrets object used by the functions-build-offchain-secrets command globalOffchainSecrets: { // DON level API Keys soundchartAppId: process.env.SOUNDCHART_APP_ID, soundchartApiKey: process.env.SOUNDCHART_API_KEY, twilioApiKey: process.env.TWILIO_API_KEY, }, // Per-node offchain secrets objects used by the functions-build-offchain-secrets command perNodeOffchainSecrets: [ { soundchartAppId: process.env.SOUNDCHART_APP_ID, soundchartApiKey: process.env.SOUNDCHART_API_KEY, twilioApiKey: process.env.TWILIO_API_KEY, }, // Node level API Keys { soundchartAppId: process.env.SOUNDCHART_APP_ID, soundchartApiKey: process.env.SOUNDCHART_API_KEY, twilioApiKey: "", }, { soundchartAppId: process.env.SOUNDCHART_APP_ID, soundchartApiKey: process.env.SOUNDCHART_API_KEY, twilioApiKey: "", }, { soundchartAppId: process.env.SOUNDCHART_APP_ID, soundchartApiKey: process.env.SOUNDCHART_API_KEY, twilioApiKey: "", }, ], }
Code Explanation
The repository includes Solidity smart contracts that run on EVM-based chains, and JavaScript code that provides tooling using the Hardhat smart contract development framework.
The sample smart contract that houses the logic for paying the music artist is in the ./contracts/sample-apps/RecordLabel.sol file. This smart contract represents the financial contract between an Artist
and the record company, and tracks artist metadata such as the listener count for a given artist.
For the purposes of this demo, we’ll create a mock stablecoin called STC, which is an ERC20 token and is used to pay the Music Artist. We use your second MetaMask wallet account as the music artist’s wallet for the purposes of this sample app.
For the RecordLabel.sol
contract, the key functionality is contained in the executeRequest()
method and the fulfillRequest()
callback. These two methods are the API for using Chainlink Functions.
executeRequest()
takes in the following typed arguments:
source string
secrets bytes
secretsLocation int
(default 0 = inline, 1 = remote URI for secrets)args string[]
subscriptionId uint64
gasLimit uint32
(100,000 to a max of 300,000)
function executeRequest( string calldata source, bytes calldata secrets, Functions.Location secretsLocation, string[] calldata args, // args in sequence are: ArtistID, artistname, lastListenerCount, artist email uint64 subscriptionId, uint32 gasLimit ) public onlyOwner returns (bytes32) { Functions.Request memory req; req.initializeRequest(Functions.Location.Inline, Functions.CodeLanguage.JavaScript, source); if (secrets.length > 0) { if (secretsLocation == Functions.Location.Inline) { req.addInlineSecrets(secrets); } else { req.addRemoteSecrets(secrets); } } if (args.length > 0) req.addArgs(args); // Update storage variables. bytes32 assignedReqID = sendRequest(req, subscriptionId, gasLimit); latestRequestId = assignedReqID; latestArtistRequestedId = args[0]; return assignedReqID; }
The custom JavaScript will be the source
argument. If that source code gets passed data, that will be in args
. Secrets like API keys will be passed in secrets
, and they are encrypted so that they cannot be misused when they’re on the public blockchain.
Finally, we will pass in a subscriptionId
—this refers to a Chainlink Functions subscription, as the execution of code by the Chainlink decentralized oracle network requires an active and funded subscription to offset the cost of execution jobs for you. Once again, it’s like a travel pass that you keep topped up, and every time you travel, some amount is taken from your balance.
We will return to Subscriptions a bit later, but for now, understand that these inputs are all you need to provide the executeRequest()
method in order to get it to run your source code on the decentralized oracle network.
Now let’s consider the kind of arbitrary code that can be supplied as part of the source
argument. For the beta, only JavaScript (specifically, NodeJS) code is supported. For our Artist / Record Label smart contract, we pass in the code that is in the ./Twilio-Spotify-Functions-Source-Example.js file.
This code pulls off the arguments passed into the executeRequest()
method in the args
parameter—this is how we pass arguments into our custom code. It then proceeds to make a HTTP Get
request to the music API that gives us the latest streaming numbers for that artist.
If the artist has grown their stream count, we send an HTTP Post request to the Twilio SendGrid email API and pass in all the details we need to send the artist an exciting payout email like this:
The code also calculates what the increase or decrease in the artist’s music streams are, and computes the amount of STC payable to the artist. The custom code depends on the Functions
library, which is injected into the global scope inside the remote code execution environment (sandboxed VM) where our custom JavaScript is being run. We must use this Functions
library to make HTTP requests, because a smart contract running on a blockchain cannot natively access off-chain APIs.
Whether or not a payment is due to the artist, we return the latest monthly stream count to the oracle nodes, where they independently arrive at consensus on the returned data, and then post that returned result back to the blockchain.
When our custom JavaScript code returns data to our consuming smart contract, it must return the data as a NodeJS Buffer type, which is how any data represented by a sequence of bytes is encoded as bytes in NodeJS. These bytes can then be decoded on-chain in the smart contract. While in beta, the custom JavaScript code must return strings
or integers
(the list of supported return data types is in the top-level README
of the repo). Once again, we use the injected Functions
library to encode return data into Buffers.
This is where the fulfillRequest()
callback method in the RecordLabel.sol
smart contract kicks in. It receives the data that the Chainlink decentralized oracle nodes achieved consensus on, and that data is used to compute the payment due to the Artist and make that payment to the Artist without any need for human intervention.
function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal override { latestError = err; emit OCRResponse(requestId, response, err); // Artist contract for payment logic here. // Artist gets a fixed rate for every addition 1000 active monthly listeners. bool nilErr = (err.length == 0); if (nilErr) { string memory artistId = latestArtistRequestedId; (int256 latestListenerCount, int256 diffListenerCount) = abi.decode(response, (int256, int256)); if (diffListenerCount <= 0) { // No payments due. return; } // Pay the artist at 'artistData[latestArtistRequestedId].walletAddress'. uint8 stcDecimals = IStableCoin(s_stc).decimals(); // Artist gets 1 STC per 10000 additional streams. uint256 amountDue = (uint256(diffListenerCount) * 1 * 10 ** stcDecimals) / 10000; console.log("\nAmount Due To Artist: ", amountDue); payArtist(artistId, amountDue); // Update Artist Mapping. artistData[artistId].lastListenerCount = uint256(latestListenerCount); artistData[artistId].lastPaidAmount = amountDue; artistData[artistId].totalPaid += amountDue; } }
Deploying the Application
Now that we have a good idea of how the code is laid out, let’s run it! The following steps are listed in the repo’s /README file.
Before we deploy the RecordLabel.sol
smart contract to the Sepolia testnet, let’s compile the code and run it in the Functions Simulator that comes bundled with the repository. This will simulate what will happen without actually going through with executing and creating transactions. More docs on the CLI commands can be found in the README, but we can run through the main steps here together.
First, run the simulator to compile the smart contracts, and simulate executing the JavaScript code.
npx hardhat functions-simulate-twilio --gaslimit 300000
The simulator should run the code as per your request config file, and print the latest monthly streaming numbers for the artist Tones & I to the console, the payout calculated, and then confirm that an email has been sent to the artist.
This whole simulation is done using a fork of the real blockchain that runs locally on your machine—which means you get to test your application end-to-end without using any gas and without having to wait for block confirmations and transactions to complete!
Next, let’s break down the various steps in setting up this application on the actual Sepolia testnet. You can also see these steps in the README, where the most up-to-date information is likely to reside.
First we need to deploy the STC token contract (SimpleStableCoin.sol
) so that we have tokens to pay the artist with. Note that most (but not all) of these tasks are in ./tasks/Functions-client-twilio-spotify/<STEP #>_<<TASK NAME>>.js
With this set up, all you have to do is run the right task to deploy or interact with the relevant smart contract on Sepolia or Mumbai testnet.
Note that SimpleStableCoin.sol
mints 1 million STC immediately on deployment and gives that to the deployer of the STC contract. This means that the tokens are held by the deployer account (your wallet account that is allowlisted when you registered for using Chainlink Functions!).
npx hardhat functions-deploy-stablecoin --network sepolia --verify true
You should see logs on your screen like this:
Take a note of your stablecoin contracts address on-chain. We will need this in future commands.
Next, let’s deploy our RecordLabel.sol
smart contract to the Sepolia testnet. Run:
npx hardhat functions-deploy-recordlabel --network sepolia --stc-contract <<0x-contract-address>> --verify true
Once deployed, take a note of your deployed RecordLabel contract’s address on your testnet. You will need to use this address for subsequent tasks as it is the “client contract”:
The next step is something specific to how ERC-20s work, but we need to approve our RecordLabel contract to spend the stablecoin that is owned by the deployer of the stablecoin (remember how 1 million STC was minted to the deployer when we deployed the STC contract?)
Therefore we need to authorize the RecordLabel contract to pay artists using those minted STCs:
npx hardhat functions-approve-spender --network sepolia --client-contract <<0x-contract-address>> --stc-contract <<0x-contract-address>>
Once successful, you should see the following:
We should also initialize some metadata relating to our artist so that the RecordLabel smart contract has some starting information on the artist, such as the artist’s wallet (which will be your second MetaMask account, for this example) and its last listener count etc. For that, we run:
npx hardhat functions-initialize-artist --network sepolia --client-contract <<0x-contract-address>>
In our example we add some metadata for the artist “Tones & I,” which we have obtained from the Soundcharts documentation, and assign them our second wallet address.
Next, we need to create and fund a Chainlink Functions subscription and register our deployed smart contract as a valid consumer contract for that subscription.
npx hardhat functions-sub-create --network sepolia --amount 2.5 --contract <<0x-client-contract-address>>
This command will do three things:
- Create a Chainlink Functions subscription.
- Register our deployed
RecordLabel
contract as a consumer. - Fund our subscription with 2.5 LINK from your Web3 wallet. Note that we recommend funding with at least 2.5 LINK to ensure that network gas price spikes don’t cause transactions to fail. Since you can always recover your test LINK when you cancel your subscription, you can put more LINK there to remove the risk of your transactions running out of LINK due to gas spikes on the testnet.
Once the subscription is created and funded, a summary of your subscription will be printed to your terminal. Take a note of your subscription ID; you will also need this for subsequent steps.
Run the Application
Now that the smart contract is deployed to the Sepolia test network, our final step is to send the code in ./Twilio-Spotify-Functions-Source-Example.js to the RecordLabel Contract to initiate Chainlink Functions’ execution. This will fetch the artist’s latest streaming numbers, calculate payouts, and post that data on-chain.
When you run the following command, be sure to replace the subscription ID value with the generated subscription ID from the previous step and the deployed contract address value with the deployed contract address from the “Deploy the Application“ section of this blog post.
npx hardhat functions-request --network sepolia --contract <<0x-client-contract-address>> --subid <__<__Subscription Id from previous step__>> --gaslimit 300000
As you can see, we pass the network, the RecordLabel contract’s address, our newly created and funded subscription ID, and also the maximum amount of gas we want Chainlink Functions to call our `fulfillRequest()` function with as parameters to the CLI tool.
This command will first run the powerful end-to-end simulator locally (unless you pass in --simulate false
). Then, if you approve submitting the transactions on-chain, the tool programmatically takes the data in the ./Functions-request-config.js file, fetches the encrypted API secrets from the URL you provided, connects to the Sepolia testnet, and sends that data to the deployed RecordLabel contract.
From there, executeRequest()
triggers a series of on-chain events that gets picked up by the Chainlink decentralized oracle network, and each node in that network independently runs the custom JavaScript code in ./Twilio-Spotify-Functions-Source-Example.js. Chainlink’s OCR consensus protocol kicks in to ensure consensus on the data returned by each node, and that agreed-upon, cryptographically verifiable response is submitted back on-chain and is sent to RecordLabel in the fulfillRequest() callback.
All of this can be seen in the terminal output when you run npx hardhat functions-request
:
To summarize this step, when we run the CLI command, the following things will happen:
- There will be an end-to-end estimation of gas and LINK costs, and you must confirm you wish to proceed by entering “Y”.
- The simulator will run an end-to-end simulation locally (i.e it internally does the same as
npx hardhat functions-simulate-twilio –gaslimit 300000
). - The
env-enc
package will connect to your GitHub account using your access token, and temporarily create a private GitHub gist file with your encrypted secrets (this gets deleted, for security reasons, after your request completes!) - The request object in the Request Config file gets sent to the
RecordLabel
contract’sexecuteRequest
method. - The Chainlink decentralized oracle network receives the request payload, parses out its pieces, and each node separately executes your custom JS code.
- The results are put through the Chainlink DON consensus algorithm, and a single final agreed response is posted back on-chain and sent to the
fulfillRequest
callback in theRecordLabel
contract. - The hex string of the returned value and its decoded value.
- A summary of the billing in LINK.
The CLI tool is extremely powerful, and you can run a bunch of other commands relating to topping up, transferring, or canceling your subscription, reading any errors from your client contract, or fetching the latest on-chain response that was returned by Chainlink Functions, etc. You can check out all the commands by following the links here.
Checking the Results
Now there are a couple of ways you can check your results!
The coolest way is to check MetaMask! Switch to the second wallet address that you used (this is the one that also receives the artist’s payments) and make sure you’ve added LINK as a recognized asset to that wallet account. Then take your STC contract address and import it as a token to your MetaMask wallet. You should see the same amount of STC in your wallet address as the amount that was printed to your terminal as being paid to the artist!
Or you could go to https://sepolia.etherscan.io/ and type in your second wallet address and look at the tokens that it has. Each time you deploy an STC contract there will be an entry because each contract address is treated as a separate token.
Conclusion
This example showcases how Chainlink Functions can be used to get data from absolutely any API and bring it on-chain, or push data in the opposite direction. You can connect your smart contract to the outside world using Chainlink Functions. You can also do powerful end-to-end simulations right from your machine without spending on test ETH or gas. You can interact with third-party services such as Spotify APIs, Twilio’s email services, and more to enhance your dApp, which unlocks new features and functionality that native Web3 applications cannot access on their own.
The code for this example is available in this repository. And you can also catch a video version of this, run as a Deep Dive Masterclass, in case you want to watch us code this up live!
To learn more about Web3 and decentralized applications, check out this blog post and this free Web3 Educational Resource. To learn more about Chainlink Functions, head to the Chainlink website.
Authors:
Zubin Pratap – Developer Relations Engineer, Chainlink Labs (Twitter)
Harry Papacharissiou – Developer Advocate Manager (Twitter)