Add examples
This commit is contained in:
parent
d7fb0658c7
commit
97956c32c8
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
typechain-types
|
||||
*.d.ts
|
210
README.md
210
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)
|
||||
|
||||
### 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
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
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
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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user