How to Build a Custom Oracle
In this tutorial, we’ll show you how to build a custom Web3 oracle using Tenderly Web3 Actions. An oracle gathers data from real-world systems and sends it to the blockchain. It acts as an entry point for streaming data from Web2 applications toward smart contracts.
Using oracles, we can access data from sources outside the blockchain (e.g., exchanges, traffic and weather data, gas rates, oil prices, etc) from within our smart contracts.
The entire code is available in this Github repo.
Project Overview
The purpose of this project is to teach you how to build a Web3 Action that fetches data from a Web2 API whenever a smart contract makes a data request through an on-chain event. This data will then be sent to the contract that requested it.
Here’s a quick breakdown of the project’s flow.
- Create a Consumer – a smart contract that requests the ETH price from an Oracle Contract.
- The Oracle Contract requests a new coin price from the CoinGecko API with the
RequestCoinPrice
event. - Once the price request has been observed, the Web3 Action is called to fetch the value.
- The price value is pushed back to the Oracle Contract.
We will use a pre-made smart contract for this project. Feel free to explore the code and play around with it in the Tenderly Sandbox and check out the Sandbox Docs.
Note that the SimpleConsumer
contract accepts the coinPrice
coming in only from the CoinOracle
it was assigned when it got deployed. Additionally, CoinOracle
accepts updates only if it is signed by the same Wallet that deployed it (owner
).
The Plan
Here’s a list of steps we’ll need to take to build our oracle:
- Deploy the
CoinOracle
andSimpleCoinConsumer
smart contracts. - Get the API key and other information needed to use one of the Ethereum providers. We’ll store this data in the Web3 Actions Secrets (needed for interaction 5).
- Get the Private Key of the account that deployed the Oracle Contract (
owner
) and store it in the Web3 Actions Secrets (needed to sign transactions in interaction 5). - Initialize Tenderly’s Web3 Actions directory on your dev environment and add npm dependencies.
- Create a Web3 Action that will send price updates to the oracle.
1: Deploy the Smart Contracts
Both CoinOracle
and SimpleCoinConsumer
contracts are stored in the same file: CoinOracle.sol
. Check out the source code in our GitHub repo.
The CoinOracle
contact must be deployed first, before the SimpleCoinConsumer
contract, preferably using a different Wallet. Make sure to pass the CoinOracle
address as the constructor parameter.
For the purpose of this tutorial, we’ll deploy to the Ropsten test network (ID is 3) but you’re free to use a different one.
2: Initialize Web3 Actions
For this tutorial, you’ll need access to the Tenderly Dashboard. If you don’t have an account, sign up here for free (no cc required as well).
Install the Tenderly CLI (follow this installation guide). With the Tenderly CLI installed on your machine, create a new directory tdly-actions
and cd
into it to initialize your Web3 Actions using the tenderly actions init
command.
$ cd tdly-actions
$ tenderly actions init
When prompted, select your Tenderly projects where you want to deploy Web3 Actions. List out the contents of the directory to explore the files created by the CLI:
ls actions
example.ts
tsconfig.json
package.json
The Tenderly CLI will also create an npm project which will allow your Web3 Actions to use any npm package.
Install NPM dependencies - we’ll use Axios to make requests to an external API and Ethers to interact with the blockchain. Run the following command from your terminal to add these dependencies to your npm project:
cd actions && npm install --save-dev axios ethers @types/node && cd ..
3: Create Your Web3 Action
Before moving forward, copy the CoinOracleContract.json
file to the actions
directory. You can find this JSON file in Remix or this GitHub repo.
All files your Web3 Action code uses and references must be placed in the
actions
directory (e.g.
CoinOracleContract.json
).
Set the CONTRACT_ADDRESS
variable to the address of your deployed Oracle
:
import { ActionFn, Context, Event, TransactionEvent } from '@tenderly/actions';
import { ethers } from 'ethers';
import CoinOracleContract from './CoinOracleContract.json';
const CONTRACT_ADDRESS = '...'; // replace with contract address
export const coinPrice: ActionFn = async (context: Context, event: Event) => {
let transactionEvent = event as TransactionEvent;
const ifc = new ethers.utils.Interface(CoinOracleContract.abi);
const { data, topics } = transactionEvent.logs[0];
const priceRequest = ifc.decodeEventLog('RequestCoinPrice', data, topics);
const price = await getPrice();
const oc = await oracleContract(context, ifc);
await oc.update(priceRequest.reqId, price, {
gasLimit: 250000,
gasPrice: ethers.utils.parseUnits('100', 'gwei'),
});
console.log(`Processed: ${priceRequest.reqId} with price in cents: ${price}`);
};
const getPrice = async (coin: string) => {
const coinInfo = await axios.get(`https://api.coingecko.com/api/v3/coins/${coin}`);
return coinInfo.data.market_data.current_price.usd * 100;
};
const oracleContract = async (context: Context, contractInterface: ethers.utils.Interface) => {
const etherscanApiKey = await context.secrets.get('oracle.providerApiKey');
const provider = ethers.getDefaultProvider(ethers.providers.getNetwork(3), {
etherscan: etherscanApiKey,
});
const oracleWallet = new ethers.Wallet(
await context.secrets.get('oracle.addressPrivateKey'),
provider,
);
const contract = new ethers.Contract(CONTRACT_ADDRESS, contractInterface, oracleWallet);
return contract;
};
Our Web3 Action will make a call to a CoinGecko API using Axios in getPrice
. The result we get from the API will be the price in cents. We’ll send this data back to our smart contract.
However, before moving forward, we have to take care of some plumbing.
Off-road: Deal with Typescript. If Typescript throws an error, add the following two lines to your tsconfig.json
file, under compilerOptions
. This will stop Typescript from frowning upon us for importing a JSON file as an ES module.
"esModuleInterop": true,
"resolveJsonModule": true
Plumbing: Instantiate the Ethers Interface. The CoinOracleContract.json
file creates an Interface
instance that Ethers uses to encode and decode data exchanged when interacting with smart contracts. We’re passing the abi
part of the entire JSON file.
Get the Event Data. Use the Interface
we just created (ifc
) to decodeEventLog
. We know that the first log entry (logs[0]
) corresponds to the RequestCoinPrice
event, so this is the one we want to decode. In more complex interactions, you may need to do this dynamically like we did here.
Necessary Web3 Plumbing aka Boilerplate. To interact with our Oracle Contract, we need to do some plumbing. The contract has a restriction - updates can be sent only by the owner
. This means we need to send transactions from the address that deployed it. The plumbing is done in the oracleContract
function, and here’s the breakdown of steps.
Plumbing: Configure a Provider Object. We can obtain the provider using ethers.getDefaultProvider
to work with network 3
(Ropsten’s ID). We’re also passing a second argument – a configuration object which contains the API key. Consult ethers docs for more information on how to configure other providers and use alternatives like JsonRpcProvider
.
Plumbing: Create a Wallet. You need to create a Wallet
to ensure that each transaction originating from our oracle is signed and funded by the same address that deployed the contract. Since the Wallet’s private key is sensitive information, we’re reading it from Secrets:
await context.secrets.get("w3_oracle.oracle_address_private_key")
Plumbing: Create a Contract. This is the step where we piece together everything we’ve created so far:
Contract
instance by passingCONTRACT_ADDRESS
.contractInterface
for encoding data we want to send to the network.oracleWallet
to sign the transaction.
🎉 Send Data Back to the Oracle Contract. With the plumbing completed, the last line invokes the function receiveWeatherUpdate
and sends the prediction our oracle has come up with. This bit issues a transaction to our smart contract.
4: Specify When Your Web3 Action Gets Executed
We want execute the transaction every time the RequestCoinPrice
event is fired by the OracleContract
on the Ropsten network. We want to do this only after the transaction has been mined.
Replace YOUR_USERNAME
and YOUR_PROJECT_SLUG
with your Tenderly username and the slug of your project. You can copy those from the Dashboard URL:
https://dashboard.tenderly.co/{YOUR_USERNAME}/{YOUR_PROJECT_SLUG}/transactions
Also, replace CONTRACT_ADDRESS
with the address of your deployed Oracle Contract:
account_id: ''
actions:
YOUR_USERNAME/YOUR_PROJECT_SLUG:
runtime: v1
sources: actions
specs:
coinOracle:
description: Swapit
function: coinOracle:coinPrice
trigger:
type: transaction
transaction:
status:
- mined
filters:
- network: 3
eventEmitted:
contract:
address: CONTRACT_ADDRESS
name: RequestCoinPrice
project_slug: ''
5: Create Web3 Action Secrets
For Web3 Actions to perform operations on the deployed OracleContract
, we need a way to access the contract. You can choose any provider service (Infura, QuickNode, Alchemy, Etherscan, etc). Check Ethers’ Default Provider docs to find out what’s needed to establish the access.
Since we’re using Etherscan as the provider, we only need the API key. The key will be stored in Web3 Actions Secrets.
Keep all your sensitive data tucked away safely in the Web3 Actions Secrets (e.g., API keys,
Wallet private keys, etc). Secrets are highly secure, so even if you try to
console.log
the values, they will remain hidden.
Secrets. Head over to your Tenderly Dashboard to place the private key you copied in Web3 Actions Secrets. From the sidebar, go to “Actions”, click “Secrets”, and then “Add New”. Name the secret oracle.providerApiKey
and paste the API token as well as any other sensitive information you need.
Store Oracle Wallet Private Key. To send transactions to the chain, our Web3 Action needs to have a private key to sign transactions and fund them. The OracleContract
is designed to only accept updates that are signed by the address that deployed them.
If you’re using Metamask, pick the account you used to deploy the smart contract and copy its private key following this Metamask guide.
In the Tenderly Dashboard, create a new Secret called w3_oracle.oracle_address_private_key
and paste the private key.
6: Deploy Your Web3 Action
Go to the root of your project (folder containing the tenderly.yaml
file) and run the following command:
tenderly actions deploy
After the CLI shows successful deployment, you can head to Tenderly Dashboard. There you should see that your Web3 Action has been deployed (or is getting ready to be deployed).
7: Execute the Project
To check if what we’ve created so far is working, head back to Remix and invoke the doSomethingSmart
function of the SimpleCoinConsumer
contract.
After a while, you should see an execution in the Execution tab in your Tenderly Dashboard. You’ll also see a new transaction that was sent from the oracle’s address by your Web3 Action, calling the update
function of the OracleContract
contract. 🎉