From 97956c32c8fbffd7bd444176ee77e20499be24ac Mon Sep 17 00:00:00 2001 From: Tornado Contrib Date: Fri, 12 Apr 2024 01:49:58 +0000 Subject: [PATCH] Add examples --- .eslintignore | 2 + README.md | 212 +++++++++++++++++++++++++++++++++++- docker-compose.yml | 4 +- docker.env | 2 +- example/index.ts | 127 +++++++++++++++++++++ package.json | 2 +- src/GasPriceOracle.abi.json | 189 ++++++++++++++++++++++++++++++++ src/index.d.ts | 2 +- src/index.ts | 2 +- 9 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 .eslintignore create mode 100644 example/index.ts create mode 100644 src/GasPriceOracle.abi.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e414e7e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +typechain-types +*.d.ts \ No newline at end of file diff --git a/README.md b/README.md index 0976a3c..20e38bd 100644 --- a/README.md +++ b/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) \ No newline at end of file +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" + } +] +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 23d4042..7d3d7ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docker.env b/docker.env index 85e5dfe..cfa4d41 100644 --- a/docker.env +++ b/docker.env @@ -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= \ No newline at end of file diff --git a/example/index.ts b/example/index.ts new file mode 100644 index 0000000..65cde8c --- /dev/null +++ b/example/index.ts @@ -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(); \ No newline at end of file diff --git a/package.json b/package.json index 006b779..b051969 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/GasPriceOracle.abi.json b/src/GasPriceOracle.abi.json new file mode 100644 index 0000000..2ef8f09 --- /dev/null +++ b/src/GasPriceOracle.abi.json @@ -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" + } +] \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts index 7d0493f..8314a4f 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,5 +1,5 @@ import { BaseContract } from 'ethers'; -import "dotenv/config"; +import 'dotenv/config'; export interface gasstation { standard: { maxPriorityFee: number; diff --git a/src/index.ts b/src/index.ts index 40baaaf..56bab4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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';