Add examples

This commit is contained in:
Tornado Contrib 2024-04-12 01:49:58 +00:00
parent d7fb0658c7
commit 97956c32c8
Signed by: tornadocontrib
GPG Key ID: 60B4DF1A076C64B1
9 changed files with 535 additions and 7 deletions

2
.eslintignore Normal file

@ -0,0 +1,2 @@
typechain-types
*.d.ts

212
README.md

@ -1,3 +1,213 @@
# gas-price-oracle
Decentralized Gas Price Oracle that proxies recommended fee value from [polygon gas station](https://docs.polygon.technology/tools/gas/polygon-gas-station/#mainnet)
Decentralized Gas Price Oracle that proxies recommended fee value from [polygon gas station](https://docs.polygon.technology/tools/gas/polygon-gas-station/#mainnet)
### Mainnet
0xf81a8d8d3581985d3969fe53bfa67074adfa8f3c
https://polygonscan.com/address/0xf81a8d8d3581985d3969fe53bfa67074adfa8f3c#readContract
### What is gas-price-oracle?
Gas price oracle is a decentralized proxy of gas station for polygon mainnet. It would update the recommended maxPriorityFeePerGas value to contracts which should be parsed from the GasPriceOracle contract.
One of the benefits of the GasPriceOracle contract is that it doesn't require centralized gas station API that would track your user-agent headers https://github.com/ethers-io/ethers.js/blob/main/src.ts/providers/network.ts#L327, or block requests very often https://github.com/ethers-io/ethers.js/issues/4320.
This would also ensure faster gas price fetching since it resolves all the necessary data by a single request. Also, there are necessary timestamp and heartbeat configuation that you could make a refer of.
You can see example/index.ts to see examples to integrate with ethers.js provider. In order to deploy on alternative chain or if you are seeking to modify the source code of the server you can refer contracts and src folder.
## ABI
```
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "GAS_UNIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_derivationThresold",
"type": "uint32"
}
],
"name": "changeDerivationThresold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_gasUnit",
"type": "uint32"
}
],
"name": "changeGasUnit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_heartbeat",
"type": "uint32"
}
],
"name": "changeHeartbeat",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "changeOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "derivationThresold",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "gasPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "heartbeat",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxFeePerGas",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxPriorityFeePerGas",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pastGasPrice",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_gasPrice",
"type": "uint32"
}
],
"name": "setGasPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "timestamp",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
}
]
```

@ -5,7 +5,7 @@ services:
container_name: maticgasstation
image: maticgasstation
restart: always
stop_grace_period: 30m
stop_grace_period: 1m
environment:
- POS_RPC=https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
- ZKEVM_RPC=https://1rpc.io/polygon/zkevm
@ -30,7 +30,7 @@ services:
container_name: gaspriceoracle
image: gaspriceoracle
restart: always
stop_grace_period: 30m
stop_grace_period: 1m
env_file:
- ./docker.env
build:

@ -1,3 +1,3 @@
GAS_STATION=http://maticgasstation:7000
GAS_STATION=http://maticgasstation:7000/v2
RPC_URL=https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
MNEMONIC=

127
example/index.ts Normal file

@ -0,0 +1,127 @@
import { JsonRpcProvider, BaseContract, Network, FetchUrlFeeDataNetworkPlugin, parseUnits } from 'ethers';
import { GasPriceOracle, GasPriceOracle__factory, Multicall3, Multicall3__factory } from '../typechain-types';
import 'dotenv/config';
const ORACLE_ADDRESS = '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C';
const MULTICALL_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
const RPC_URL = process.env.RPC_URL || 'https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607';
const GAS_STATION = process.env.GAS_STATION || 'https://gasstation.polygon.technology/v2';
export interface Call3 {
contract: BaseContract;
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any[];
allowFailure?: boolean;
}
export async function multicall(Multicall: Multicall3, calls: Call3[]) {
const calldata = calls.map((call) => ({
target: call.contract.target,
callData: call.contract.interface.encodeFunctionData(call.name, call.params),
allowFailure: call.allowFailure ?? false
}));
const returnData = await Multicall.aggregate3.staticCall(calldata);
const res = returnData.map((call, i) => {
const [result, data] = call;
const decodeResult = (result && data && data !== '0x') ? calls[i].contract.interface.decodeFunctionResult(calls[i].name, data) : null;
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult;
});
return res;
}
// caching to improve performance
const oracleMapper = new Map();
const multicallMapper = new Map();
export function getGasOraclePlugin(networkKey: string, gasStation: string) {
return new FetchUrlFeeDataNetworkPlugin(gasStation, async (fetchFeeData, provider, request) => {
if (!oracleMapper.has(networkKey)) {
oracleMapper.set(networkKey, GasPriceOracle__factory.connect(ORACLE_ADDRESS, provider));
}
if (!multicallMapper.has(networkKey)) {
multicallMapper.set(networkKey, Multicall3__factory.connect(MULTICALL_ADDRESS, provider));
}
const Oracle = oracleMapper.get(networkKey) as GasPriceOracle;
const Multicall = multicallMapper.get(networkKey) as Multicall3;
const [timestamp, heartbeat, feePerGas, priorityFeePerGas] = await multicall(Multicall, [
{
contract: Oracle,
name: 'timestamp'
},
{
contract: Oracle,
name: 'heartbeat'
},
{
contract: Oracle,
name: 'maxFeePerGas'
},
{
contract: Oracle,
name: 'maxPriorityFeePerGas'
},
]);
const isOutdated = Number(timestamp) <= (Date.now() / 1000) - Number(heartbeat);
if (!isOutdated) {
const maxPriorityFeePerGas = priorityFeePerGas * 13n / 10n;
const maxFeePerGas = feePerGas * 2n + maxPriorityFeePerGas;
console.log(`Fetched from oracle: ${JSON.stringify({
gasPrice: feePerGas.toString(),
maxFeePerGas: feePerGas.toString(),
maxPriorityFeePerGas: priorityFeePerGas.toString(),
})}`);
return {
gasPrice: maxFeePerGas,
maxFeePerGas,
maxPriorityFeePerGas
};
}
// Prevent Cloudflare from blocking our request in node.js
request.setHeader('User-Agent', 'ethers');
const [ { bodyJson: { fast }}, { gasPrice } ] = await Promise.all([
request.send(), fetchFeeData()
]);
console.log(`Fetched from gasStation: ${JSON.stringify({
gasPrice: gasPrice ? gasPrice.toString() : undefined,
maxFeePerGas: parseUnits(`${fast.maxFee}`, 9).toString(),
maxPriorityFeePerGas: parseUnits(`${fast.maxPriorityFee}`, 9).toString(),
})}`);
return {
gasPrice,
maxFeePerGas: parseUnits(`${fast.maxFee}`, 9),
maxPriorityFeePerGas: parseUnits(`${fast.maxPriorityFee}`, 9),
};
});
}
export async function start() {
const previousNetwork = await new JsonRpcProvider(RPC_URL).getNetwork();
const network = new Network(previousNetwork.name, previousNetwork.chainId);
network.attachPlugin(getGasOraclePlugin(`${previousNetwork.chainId}_${RPC_URL}`, GAS_STATION));
const provider = new JsonRpcProvider(RPC_URL, network, { staticNetwork: network });
provider.pollingInterval = 1000;
provider.on('block', async () => {
console.log(await provider.getFeeData());
});
}
start();

@ -8,7 +8,7 @@
"compile": "hardhat compile && hardhat flatten:all",
"build": "yarn compile && yarn types",
"start": "ts-node ./src/index.ts",
"lint": "eslint . --ext .ts --ignore-pattern typechain-types"
"lint": "eslint . --ext .ts"
},
"files": [
"contracts",

189
src/GasPriceOracle.abi.json Normal file

@ -0,0 +1,189 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "GAS_UNIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_derivationThresold",
"type": "uint32"
}
],
"name": "changeDerivationThresold",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_gasUnit",
"type": "uint32"
}
],
"name": "changeGasUnit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_heartbeat",
"type": "uint32"
}
],
"name": "changeHeartbeat",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"name": "changeOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "derivationThresold",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "gasPrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "heartbeat",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxFeePerGas",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxPriorityFeePerGas",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pastGasPrice",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint32",
"name": "_gasPrice",
"type": "uint32"
}
],
"name": "setGasPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "timestamp",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
}
]

2
src/index.d.ts vendored

@ -1,5 +1,5 @@
import { BaseContract } from 'ethers';
import "dotenv/config";
import 'dotenv/config';
export interface gasstation {
standard: {
maxPriorityFee: number;

@ -1,6 +1,6 @@
import { JsonRpcProvider, Wallet, HDNodeWallet, BaseContract, parseUnits } from 'ethers';
import { GasPriceOracle, GasPriceOracle__factory, Multicall3, Multicall3__factory } from '../typechain-types';
import "dotenv/config"
import 'dotenv/config';
const ORACLE_ADDRESS = process.env.ORACLE_ADDRESS || '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C';