In we created the functionality for our lottery dApp. In Part 2, we’ll be integrating the into our contract and deploying it onto the . Alternatively, you may use another supported in place of Goerli by following the same steps and substituting accordingly. Part 1 API3 QRNG Ethereum Goerli public testnet chain We’ll be using the free API3 QRNG to get truly random numbers into our contract. API3 QRNG uses to allow ANU to provide their service to blockchain and Web3 use cases without needing any 3rd party intermediaries. Airnode Forking As mentioned in Part 1, is an that allows us to deploy smart contracts to EVM networks, and in this case, spin up a locally running blockchain instance for testing our contract. We can do this by configuring Hardhat to the , which will copy the of the public network locally. We’ll need a , or an endpoint that connects Hardhat to the blockchain. We’ll be using below as our RPC provider. Hardhat EVM development environment “fork” Ethereum Goerli testnet state RPC Alchemy 1. DotEnv We’re going to use sensitive credentials in the next steps. We’ll be using the package to store those credentials as separately from our application code: DotEnv environment variables npm install dotenv Next, make a file at the root of your project. .env Make an account. Click the “CREATE APP” button and select the Goerli testnet from the dropdown menu, as the rest of the available testnets are . Insert your newly-generated Goerli RPC endpoint URL to your file: Alchemy deprecated or will soon be deprecated .env RPC_URL="{PUT RPC URL HERE}" Then add the following to the top of your file to make values in the file accessible in our code: hardhat.config.js .env require("dotenv").config(); 2. Configure Hardhat to use forking By adding the following to our in the file, we tell Hardhat to of the for use in local testing: module.exports hardhat.config.js make a copy Goerli network module.exports = { solidity: "0.8.9", networks: { hardhat: { // Hardhat local network chainId: 5, // Force the ChainID to be 5 (Goerli) in testing forking: { // Configure the forking behavior url: process.env.RPC_URL, // Using the RPC_URL from the .env }, }, }, }; Turn contract into an Airnode Requester As a requester, our contract will be able to make requests to an Airnode, specifically the , using the . It may be helpful to take a little time familiarize yourself if you haven't already. Lottery.sol ANU QRNG Airnode Request-Response Protocol (RRP) 1. Install dependencies npm install @api3/airnode-protocol 2. Import the into contract Airnode Requester At the top of , below the solidity version, import the from the : Lottery.sol Airnode RRP Contract npm registry // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; import "@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol"; contract Lottery is RrpRequesterV0{ We need to set the public of the Airnode RRP contract on the chain that we’re using. We can do this in the constructor by making the RRP address an argument: address constructor(uint256 _endTime, address _airnodeRrpAddress) RrpRequesterV0(_airnodeRrpAddress) { endTime = _endTime; } 3. Test At the top of the file, import the : test/Lottery.js Airnode protocol package const airnodeProtocol = require("@api3/airnode-protocol"); We need to pass the of the RRP contract into the constructor. We can do that by adding the following to our “Deploys” test: address it("Deploys", async function () { const Lottery = await ethers.getContractFactory("Lottery"); accounts = await ethers.getSigners(); nextWeek = Math.floor(Date.now() / 1000) + 604800; // Get the chainId we are using in Hardhat let { chainId } = await ethers.provider.getNetwork(); // Get the AirnodeRrp address for the chainId const rrpAddress = airnodeProtocol.AirnodeRrpAddresses[chainId]; lotteryContract = await Lottery.deploy(nextWeek, rrpAddress); expect(await lotteryContract.deployed()).to.be.ok; }); Run to try it out. npx hardhat test Set up Airnode 1. Parameters Let’s add our to our contract. In , add the following to the global variables: Airnode Parameters Lottery.sol // ANU's Airnode address address public constant airnodeAddress = 0x9d3C147cA16DB954873A498e0af5852AB39139f2; // ANU's uint256 endpointId bytes32 public constant endpointId = 0xfb6d017bb87991b7495f563db3c8cf59ff87b09781947bb1e417006ad7f55a78; // We'll store the sponsor wallet here later address payable public sponsorWallet; The and of a particular Airnode can be found in the documentation of the API provider, which in this case is . We’ll set them as since they shouldn’t change. airnodeAddress endpointID API3 QRNG constant 2. Set the sponsor wallet To pay for the fulfillment of Airnode requests, normally we’ll need to , our contract. In this case, if we use the contract address itself as the sponsor, it automatically sponsors itself. sponsor the requester Lottery.sol We’ll need to make our contract “Ownable”. That will allow us to restrict access for setting the sponsorWallet to the contract owner (the wallet/account that deployed the contract). First, import the at the top of : Ownable contract Lottery.sol import "@api3/airnode-protocol/contracts/rrp/requesters/RrpRequesterV0.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Lottery is RrpRequesterV0, Ownable { Then we can make our function and attach the modifier to restrict access: setSponsorWallet onlyOwner function setSponsorWallet(address payable _sponsorWallet) external onlyOwner { sponsorWallet = _sponsorWallet; } 3. Test We’ll be deriving our sponsorWallet used for funding Airnode transactions using functions from the @api3/airnode-admin package/CLI tool. npm install @api3/airnode-admin Then we can import it into our file: tests/Lottery.js const airnodeAdmin = require("@api3/airnode-admin"); We’ll hardcode the to . Add the following test underneath the "Deploys" test: ANU QRNG’s Xpub and Airnode Address derive our sponsorWalletAddress it("Sets sponsor wallet", async function () { const anuXpub = "xpub6DXSDTZBd4aPVXnv6Q3SmnGUweFv6j24SK7 7W4qrSFuhGgi666awUiXakjXruUSCDQhhctVG7AQt67gMdaRAsDnDXv23bBRKsMWvRzo6kbf" const anuAirnodeAddress = "0x9d3C147cA16DB954873A498e0af5852AB39139f2" const sponsorWalletAddress = await airnodeAdmin.deriveSponsorWalletAddress( anuXpub, anuAirnodeAddress, lotteryContract.address // used as the sponsor ); await expect(lotteryContract.connect(accounts[1]) .setSponsorWallet(sponsorWalletAddress)).to.be.reverted; await lotteryContract.setSponsorWallet(sponsorWalletAddress); expect(await lotteryContract.sponsorWallet()) .to.equal(sponsorWalletAddress); }); run to test your code. npx hardhat test Write request function In the contract, add the following function: Lottery.sol function getWinningNumber() external payable { // require(block.timestamp > endTime, "Lottery has not ended"); require(msg.value >= 0.01 ether, "Please top up sponsor wallet"); bytes32 requestId = airnodeRrp.makeFullRequest( airnodeAddress, endpointId, address(this), // Sponsor sponsorWallet, address(this), // Return the response to this contract this.closeWeek.selector, // Fulfill function "" // No params ); pendingRequestIds[requestId] = true; emit RequestedRandomNumber(requestId); sponsorWallet.call{value: msg.value}(""); } We’ll leave line 2 commented out for ease of testing. Line 3 we require users send some extra gas to the sponsor wallet so that Airnode has the gas to call our fulfill function, in this case . closeWeek We put our into the function and receive a . In line 13, we map the request ID to a boolean signifying the status of the request. Then we emit an for a request being made. Finally, we send the funds included in the transaction to the sponsor wallet. request params makeFullRequest requestId event 1. Map pending request IDs In line 13 we are storing the in a mapping. This will allow us to check whether or not the request is pending. Let's add the following under our mappings: [requestId](https://docs.api3.org/airnode/v0.7/concepts/request.html#requestid) mapping (bytes32 => bool) public pendingRequestIds; 2. Create event In line 14 we emit an event that the request has been made and a request ID has been generated. Solidity events are logged as transactions to the EVM. We need to describe our event at the top of our contract: contract Lottery is RrpRequesterV0, Ownable { event RequestedRandomNumber(bytes32 indexed requestId); Rewrite function fulfill Let’s overwrite the function: closeWeek function closeWeek(bytes32 requestId, bytes calldata data) external onlyAirnodeRrp // Only AirnodeRrp can call this function { // If valid request, delete the request Id from our mapping require(pendingRequestIds[requestId], "No such request made"); delete pendingRequestIds[requestId]; // Decode data returned by Airnode uint256 _randomNumber = abi.decode(data, (uint256)) % MAX_NUMBER; // Emit an event that the response has been received emit ReceivedRandomNumber(requestId, _randomNumber); // Prevent duplicate closings. If someone closed it first it // will increment the end time and throw. // require(block.timestamp > endTime, "Lottery is open"); // The rest we can leave unchanged winningNumber[week] = _randomNumber; address[] memory winners = tickets[week][_randomNumber]; week++; endTime += 7 days; if (winners.length > 0) { uint256 earnings = pot / winners.length; pot = 0; for (uint256 i = 0; i < winners.length; i++) { payable(winners[i]).call{value: earnings}(""); } } } In the first line set the function to take in the request ID and the payload ( ) as arguments. On line 3, we add a modifier to restrict this function to only be accessible by Airnode RRP. On line 6 and 7 we handle the request ID. If the request ID is not in the mapping, we throw an error, otherwise we delete the request ID from the mapping. data pendingRequestIds pendingRequestIds On line 9 we decode and typecast the random number from the payload. We don’t need to import anything to use . Then we use the ( ) to ensure that the random number is between 0 and . [abi.decode()](https://docs.api3.org/airnode/v0.7/reference/specifications/airnode-abi-specifications.html) modulus operator % MAX_NUMBER Line 14 will prevent duplicate requests from being fulfilled. If more than 1 request is made at the same time, the first one to be fulfilled will increment and the rest will revert. We’ll leave it commented out, for now, to make testing easy. endTime In line 11 we emit an event that the random number has been received. We need to describe our event at the top of our contract under our other event: event ReceivedRandomNumber(bytes32 indexed requestId, uint256 randomNumber); Hardhat-Deploy We’ll be using Hardhat-Deploy to deploy and manage our contracts on different chains. First let’s install the : [hardhat-deploy](https://www.npmjs.com/package/hardhat-deploy) package 1. Install npm install -D hardhat-deploy Then at the top of your file add the following: hardhat.config.js require("hardhat-deploy"); Now we can create a folder named in our root to house all of our deployment scripts. Hardhat-Deploy will run all of our deployment scripts in order each time we run . deploy npx hardhat deploy 2. Write deploy script In our folder, create a file named . We'll be using Hardhat and the Airnode Protocol package so let’s import them at the top: deploy 1_deploy.js const hre = require("hardhat"); const airnodeProtocol = require("@api3/airnode-protocol"); should be done through a function. We’ll use the to retrieve the needed as an argument to deploy our lottery contract. We'll use , a function included in Hardhat-Deploy, to get the chain ID, which we'd set to 5 in . Hardhat deploy scripts module.exports Airnode Protocol package RRP Contract address hre.getChainId() hardhat.config.js Finally, we’ll deploy the contract using . We pass in our arguments, a "from" address, and set logging to . hre.deployments true module.exports = async () => { const airnodeRrpAddress = airnodeProtocol.AirnodeRrpAddresses[await hre.getChainId()]; nextWeek = Math.floor(Date.now() / 1000) + 9000; const lotteryContract = await hre.deployments.deploy("Lottery", { args: [nextWeek, airnodeRrpAddress], // Constructor arguments from: (await getUnnamedAccounts())[0], log: true, }); console.log(`Deployed Contract at ${lotteryContract.address}`); }; Finally, let’s name our script at the bottom of our file: 1_deploy.js module.exports.tags = ["deploy"]; 3. Test locally Let’s try it out! We should test on a local blockchain first to make things easy. First let’s start up a local blockchain. We use the flag to prevent Hardhat-Deploy from running the deployment scripts each time you spin up a local node: --no-deploy npx hardhat node --no-deploy Then, in a separate terminal, we can deploy to our chain (localhost), specified by the parameter: --network npx hardhat --network localhost deploy If everything worked well, we should see a message in the console that says our contract address. We can also check the terminal running the chain for more detailed logging. Be sure to leave your blockchain running, as we’ll be using it throughout the rest of this tutorial. 4. Set sponsor wallet on deployment We can couple another script with our deployment script so that the function is called after each deployment. We’ll start by creating a file in the folder called . setSponsorWallet deploy 2_set_sponsorWallet.js We’ll be using the in this script. Let’s import them at the top: Airnode Admin package const hre = require("hardhat"); const airnodeAdmin = require("@api3/airnode-admin"); Now let’s make our function that sets the sponsor wallet. First we'll use Ethers to get a wallet. We can use to retrieve past deployments thanks to Hardhat-Deploy. Next, we instantiate our deployed contract within our script. Our deployed contract is ready to be interacted with! module.exports hre.deployments.get Let’s derive our sponsor wallet so that we can pass it into our function. We’ll follow the same steps we used to set the sponsor wallet in our tests earlier. We’ll hardcode the xpub and ANU Airnode address in lines 11 and 12. setSponsorWallet module.exports = async () => { const [account] = await hre.ethers.getSigners(); // Get the deployed contract const Lottery = await hre.deployments.get("Lottery"); const lotteryContract = new hre.ethers.Contract( Lottery.address, Lottery.abi, account ); const sponsorWalletAddress = await airnodeAdmin.deriveSponsorWalletAddress( "xpub6DXSDTZBd4aPVXnv6Q3SmnGUweFv6j24SK77W4qrSFuhGgi666awUiXakjXruUSCDQhhctVG7AQt67gMdaRAsDnDXv23bBRKsMWvRzo6kbf", "0x9d3C147cA16DB954873A498e0af5852AB39139f2", lotteryContract.address ); const tx = await lotteryContract.setSponsorWallet(sponsorWalletAddress); await tx.wait(); console.log(`Sponsor wallet set to: ${sponsorWalletAddress}`); }; Now let’s add a Hardhat-Deploy tag to our script so that it runs after each deployment: module.exports.tags = ["setup"]; Let’s try it out! npx hardhat --network localhost deploy Live testing! In this step, we’ll be testing our contract by , allowing others to interact with our contract. This will allow our random number requests to be answered by the . deploying it to a live testnet blockchain ANU QRNG Airnode 1. Enter script We need to write a script that will connect to our deployed contract and enter the lottery. A lot of the code in these steps will be similar to the tests we wrote earlier. We’ll start by creating a file in the folder named . If you look inside of the boilerplate file, you'll see Hardhat recommends a format for scripts: scripts enter.js deploy.js const hre = require("hardhat"); async function main() { // Your script logic here... } main().catch((error) => { console.error(error); process.exitCode = 1; }); Inside the funtion, we can put our "enter" script: main const [account] = await hre.ethers.getSigners(); // Get "Lottery" contract deployed via Hardhat-Deploy and connect // the account to it const Lottery = await hre.deployments.get("Lottery"); const lotteryContract = new hre.ethers.Contract( Lottery.address, Lottery.abi, account ); const ticketPrice = await lotteryContract.ticketPrice(); const guess = 55; // The number we choose for our lottery entry const tx = await lotteryContract.enter( guess, { value: ticketPrice } // Include the ticket price ); await tx.wait(); // Wait for the transaction to be mined const entries = await lotteryContract.getEntriesForNumber(guess, 1); console.log(`Guesses for ${guess}: ${entries}`); We can try it out by running the script against our local deployment: npx hardhat --network localhost run scripts/enter.js 2. Close Lottery script Next, we need a way for people to trigger Airnode for a random number when the lottery is closable. We’ll start by creating a file in the folder named and adding the boilerplate script code from the last step to it. scripts close.js In our function, we’ll instantiate our contract again. We’ll call the function in our contract to trigger a random number request. This function emits an event that we can listen to for our request ID which we’ll use to listen for a response. main getWinningNumber When we do hear a response, we can call to retrieve the winning random number for week 1! winningNumber(1) const Lottery = await hre.deployments.get("Lottery"); const lotteryContract = new hre.ethers.Contract( Lottery.address, Lottery.abi, (await hre.ethers.getSigners())[0] ); console.log("Making request for random number..."); // Call function and use the tx receipt to parse the requestId const receipt = await lotteryContract.getWinningNumber({ value: ethers.utils.parseEther("0.01"), }); // Retrieve request ID from receipt const requestId = await new Promise((resolve) => hre.ethers.provider.once(receipt.hash, (tx) => { const log = tx.logs.find((log) => log.address === lotteryContract.address); const parsedLog = lotteryContract.interface.parseLog(log); resolve(parsedLog.args.requestId); }) ); console.log(`Request made! Request ID: ${requestId}`); // Wait for the fulfillment transaction to be confirmed and read // the logs to get the random number await new Promise((resolve) => hre.ethers.provider.once( lotteryContract.filters.ReceivedRandomNumber(requestId, null), resolve ) ); const winningNumber = await lotteryContract.winningNumber(1); console.log("Fulfillment is confirmed!"); console.log(winningNumber.toString()); If we test this against our local chain, we should receive a request ID but no response. That’s because the ANU Airnode can’t access requests on our local chain. npx hardhat --network localhost run scripts/close.js You can kill the request process after the request ID is printed. 3. Set up Goerli In this next step, we’ll be pointing Hardhat towards the Goerli testnet, which will provide a shared staging environment that mimics mainnet without using real money. This means we’ll need a wallet with some Goerli ETH funds on it. Even if you have a wallet, it is highly recommended that you create a new wallet for testing purposes. ⚠️ Never use a wallet with real funds on it for development! ⚠️ First, let’s generate the wallet. We’ll use the to generate a mnemonic, but feel free to create a wallet in any way you see fit. Airnode Admin CLI npx @api3/airnode-admin generate-mnemonic # Output This mnemonic is created locally on your machine using "ethers.Wallet.createRandom" under the hood. Make sure to back it up securely, e.g., by writing it down on a piece of paper: genius session popular ... # Our mnemonic # The public address to our wallet The Airnode address for this mnemonic is: 0x1a942424D880... # The Xpub of our wallet The Airnode xpub for this mnemonic is: xpub6BmYykrmWHAhSFk... We’ll be using the mnemonic and Airnode address (public address). Let’s add our mnemonic to the file so that we can use it safely: .env MNEMONIC="genius session popular ..." Next, we’ll configure Hardhat to use the Goerli network and our mnemonic. Inside the object in our file, modify the to add a network entry: networks hardhat.config.js module.exports module.exports = { solidity: "0.8.9", networks: { hardhat: { chainId: 5, forking: { url: process.env.RPC_URL, } }, goerli: { url: process.env.RPC_URL, // Reuse our Goerli RPC URL accounts: { mnemonic: process.env.MNEMONIC } // Our mnemonic } } }; Now we can run all of our commands with the added flag without needing to change any code! --network goerli 4. Get Goerli ETH If you attempted to run any commands against Goerli, chances are that they failed. That’s because we are using our newly generated wallet that doesn’t have the funds to pay for the transaction. We can get some free Goerli ETH for testing by using a Goerli faucet. requires an Alchemy account, and requires a Twitter or Facebook account. This faucet this faucet We’ll paste the public address ( ) from our wallet generation step into either or both faucets: not mnemonic! We can test our accounts in Hardhat by using tasks. Inside of the file, underneath our imports and above our exports, add the following: hardhat.config.js task( "balance", "Prints the balance of the first account", async (taskArgs, hre) => { const [account] = await hre.ethers.getSigners(); const balance = await account.getBalance(); console.log( `${account.address}: (${hre.ethers.utils.formatEther(balance)} ETH)`); } ); Now we can run the task and see the balance of our account: balance npx hardhat --network goerli balance If you followed the faucet steps correctly (and the faucet is currently operating), you should see the balance of our account is greater than 0 ETH. If not, you may need to wait a little bit longer or try a different faucet. 0x0EDA9399c969...: (0.5 ETH) 5. Use Lottery contract on public chain We have everything configured to deploy onto a public chain. Let’s start with the deployment command: npx hardhat --network goerli deploy Keep in mind things will move much slower on the Goerli network. Next we’ll enter our lottery: npx hardhat --network goerli run ./scripts/enter.js And finally, close our lottery: npx hardhat --network goerli run ./scripts/close.js Conclusion This is the end of the tutorial, I hope you learned something! The complete code can be found in the (Feel free to drop a ⭐️). If you have any questions, please feel free to join the API3 . tutorial repo Discord Also Published Here