onchain oracle support
This commit is contained in:
parent
beea051c8a
commit
2bcfa0003d
26
package-lock.json
generated
26
package-lock.json
generated
@ -42,6 +42,12 @@
|
|||||||
"integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==",
|
"integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/mockery": {
|
||||||
|
"version": "1.4.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mockery/-/mockery-1.4.29.tgz",
|
||||||
|
"integrity": "sha1-m6It838H43gP/4Ux0aOOYz+UV6U=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.0.5",
|
"version": "14.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz",
|
||||||
@ -118,6 +124,11 @@
|
|||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bignumber.js": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
|
||||||
|
},
|
||||||
"binary-extensions": {
|
"binary-extensions": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
|
||||||
@ -180,6 +191,15 @@
|
|||||||
"type-detect": "^4.0.5"
|
"type-detect": "^4.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"chai-as-promised": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"check-error": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
@ -774,6 +794,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mockery": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mockery/-/mockery-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"prepublishOnly": "npm run lint",
|
"prepublishOnly": "npm run lint",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha -r ts-node/register tests/*.test.ts",
|
"test": "mocha --timeout 30000 -r ts-node/register tests/*.test.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "tslint -p tsconfig.json"
|
"lint": "tslint -p tsconfig.json"
|
||||||
},
|
},
|
||||||
@ -16,14 +16,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.11",
|
"@types/chai": "^4.2.11",
|
||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
|
"@types/mockery": "^1.4.29",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
"chai-as-promised": "^7.1.1",
|
||||||
"mocha": "^7.2.0",
|
"mocha": "^7.2.0",
|
||||||
|
"mockery": "^2.1.0",
|
||||||
"tslint": "^6.1.2",
|
"tslint": "^6.1.2",
|
||||||
"tslint-config-standard": "^9.0.0"
|
"tslint-config-standard": "^9.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.0.5",
|
"@types/node": "^14.0.5",
|
||||||
"@types/node-fetch": "^2.5.7",
|
"@types/node-fetch": "^2.5.7",
|
||||||
|
"bignumber.js": "^9.0.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"ts-node": "^8.10.1",
|
"ts-node": "^8.10.1",
|
||||||
"typescript": "^3.9.3"
|
"typescript": "^3.9.3"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { Oracle } from './types';
|
import { OffChainOracle, OnChainOracle } from './types';
|
||||||
|
|
||||||
const ethgasstation: Oracle = {
|
const ethgasstation: OffChainOracle = {
|
||||||
name: 'ethgasstation',
|
name: 'ethgasstation',
|
||||||
url: 'https://ethgasstation.info/json/ethgasAPI.json',
|
url: 'https://ethgasstation.info/json/ethgasAPI.json',
|
||||||
instantPropertyName: 'fastest',
|
instantPropertyName: 'fastest',
|
||||||
@ -11,7 +11,7 @@ const ethgasstation: Oracle = {
|
|||||||
denominator: 10
|
denominator: 10
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoltu: Oracle = {
|
const zoltu: OffChainOracle = {
|
||||||
name: 'zoltu',
|
name: 'zoltu',
|
||||||
url: 'https://gas-oracle.zoltu.io/',
|
url: 'https://gas-oracle.zoltu.io/',
|
||||||
instantPropertyName: 'percentile_99',
|
instantPropertyName: 'percentile_99',
|
||||||
@ -21,8 +21,18 @@ const zoltu: Oracle = {
|
|||||||
denominator: 1
|
denominator: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const chainLink: OnChainOracle = {
|
||||||
|
name: 'chainLink',
|
||||||
|
callData: '0x50d25bcd',
|
||||||
|
contract: '0xA417221ef64b1549575C977764E651c9FAB50141',
|
||||||
|
denominator: '1000000000'
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oracles: [
|
offChainOracles: [
|
||||||
ethgasstation, zoltu
|
ethgasstation, zoltu
|
||||||
|
],
|
||||||
|
onChainOracles: [
|
||||||
|
chainLink
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
73
src/index.ts
73
src/index.ts
@ -1,6 +1,7 @@
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import { GasPrice, Oracle } from './types';
|
import { GasPrice, OffChainOracle, OnChainOracle } from './types';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
export class GasPriceOracle {
|
export class GasPriceOracle {
|
||||||
lastGasPrice: GasPrice = {
|
lastGasPrice: GasPrice = {
|
||||||
@ -9,9 +10,16 @@ export class GasPriceOracle {
|
|||||||
standard: 10,
|
standard: 10,
|
||||||
low: 1
|
low: 1
|
||||||
};
|
};
|
||||||
|
defaultRpc = 'https://api.mycryptoapi.com/eth';
|
||||||
|
|
||||||
async fetchGasPrices(): Promise<GasPrice> {
|
constructor(defaultRpc?: string) {
|
||||||
for (let oracle of config.oracles) {
|
if (defaultRpc) {
|
||||||
|
this.defaultRpc = defaultRpc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchGasPricesOffChain(throwIfFailsToFetch = true): Promise<GasPrice> {
|
||||||
|
for (let oracle of config.offChainOracles) {
|
||||||
const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle;
|
const { name, url, instantPropertyName, fastPropertyName, standardPropertyName, lowPropertyName, denominator } = oracle;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
@ -27,6 +35,7 @@ export class GasPriceOracle {
|
|||||||
low: parseFloat(gas[lowPropertyName]) / denominator
|
low: parseFloat(gas[lowPropertyName]) / denominator
|
||||||
};
|
};
|
||||||
this.lastGasPrice = gasPrices;
|
this.lastGasPrice = gasPrices;
|
||||||
|
return this.lastGasPrice;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
|
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
|
||||||
}
|
}
|
||||||
@ -34,11 +43,63 @@ export class GasPriceOracle {
|
|||||||
console.error(e.message);
|
console.error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: additional arg `throwIfFailsToFetch` that throws if it fails to fetch from all oracles
|
if (throwIfFailsToFetch) {
|
||||||
|
throw new Error('All oracles are down. Probaly network error.');
|
||||||
|
}
|
||||||
return this.lastGasPrice;
|
return this.lastGasPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
addOracle(oracle: Oracle) {
|
async fetchGasPricesOnChain(throwIfFailsToFetch = true): Promise<GasPrice> {
|
||||||
config.oracles.push(oracle);
|
for (let oracle of config.onChainOracles) {
|
||||||
|
const { name, callData, contract, denominator } = oracle;
|
||||||
|
let { rpc } = oracle;
|
||||||
|
rpc = rpc ? rpc : this.defaultRpc;
|
||||||
|
const body = { jsonrpc: '2.0',
|
||||||
|
id: 1337,
|
||||||
|
method: 'eth_call',
|
||||||
|
params: [{ 'data': callData, 'to': contract }, 'latest']
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await fetch(rpc, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
const { result } = await response.json();
|
||||||
|
let fastGasPrice = new BigNumber(result);
|
||||||
|
if (fastGasPrice.isZero()) {
|
||||||
|
throw new Error(`${name} oracle provides corrupted values`);
|
||||||
|
}
|
||||||
|
fastGasPrice = fastGasPrice.div(denominator);
|
||||||
|
const gasPrices: GasPrice = {
|
||||||
|
instant: fastGasPrice.multipliedBy(1.3).toNumber(),
|
||||||
|
fast: fastGasPrice.toNumber(),
|
||||||
|
standard: fastGasPrice.multipliedBy(0.85).toNumber(),
|
||||||
|
low: fastGasPrice.multipliedBy(0.5).toNumber()
|
||||||
|
};
|
||||||
|
this.lastGasPrice = gasPrices;
|
||||||
|
return this.lastGasPrice;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (throwIfFailsToFetch) {
|
||||||
|
throw new Error('All oracles are down. Probaly network error.');
|
||||||
|
}
|
||||||
|
return this.lastGasPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
addOffChainOracle(oracle: OffChainOracle) {
|
||||||
|
config.offChainOracles.push(oracle);
|
||||||
|
}
|
||||||
|
|
||||||
|
addOnChainOracle(oracle: OnChainOracle) {
|
||||||
|
config.onChainOracles.push(oracle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/types.ts
24
src/types.ts
@ -1,11 +1,19 @@
|
|||||||
export type Oracle = {
|
export type OffChainOracle = {
|
||||||
name: string
|
name: string;
|
||||||
url: string
|
url: string;
|
||||||
instantPropertyName: string,
|
instantPropertyName: string;
|
||||||
fastPropertyName: string,
|
fastPropertyName: string;
|
||||||
standardPropertyName: string,
|
standardPropertyName: string;
|
||||||
lowPropertyName: string,
|
lowPropertyName: string;
|
||||||
denominator: number
|
denominator: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnChainOracle = {
|
||||||
|
name: string;
|
||||||
|
rpc?: string;
|
||||||
|
contract: string;
|
||||||
|
callData: string;
|
||||||
|
denominator: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GasPrice = {
|
export type GasPrice = {
|
||||||
|
@ -1,10 +1,91 @@
|
|||||||
import { GasPriceOracle } from '../src/index';
|
import { GasPriceOracle } from '../src/index';
|
||||||
import { GasPrice } from '../src/types';
|
import { GasPrice } from '../src/types';
|
||||||
|
import mockery from 'mockery';
|
||||||
|
import chai from 'chai';
|
||||||
|
|
||||||
describe('Get gas price', function () {
|
chai.use(require('chai-as-promised'));
|
||||||
|
chai.should();
|
||||||
|
|
||||||
|
describe('fetchGasPricesOffChain', function () {
|
||||||
it('should work', async function () {
|
it('should work', async function () {
|
||||||
const oracle = new GasPriceOracle();
|
const oracle = new GasPriceOracle();
|
||||||
const gas: GasPrice = await oracle.fetchGasPrices();
|
const gas: GasPrice = await oracle.fetchGasPricesOffChain();
|
||||||
console.log(gas);
|
|
||||||
|
gas.instant.should.be.a('number');
|
||||||
|
gas.fast.should.be.a('number');
|
||||||
|
gas.standard.should.be.a('number');
|
||||||
|
gas.low.should.be.a('number');
|
||||||
|
|
||||||
|
gas.instant.should.be.above(gas.fast);
|
||||||
|
gas.fast.should.be.above(gas.standard);
|
||||||
|
gas.standard.should.be.above(gas.low);
|
||||||
|
gas.low.should.not.be.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('throw checks', function () {
|
||||||
|
before('before', function () {
|
||||||
|
// Mocking the mod1 module
|
||||||
|
let fetchMock = () => {
|
||||||
|
throw new Error('Mocked for tests');
|
||||||
|
};
|
||||||
|
|
||||||
|
// replace the module with mock for any `require`
|
||||||
|
mockery.registerMock('node-fetch', fetchMock);
|
||||||
|
|
||||||
|
// set additional parameters
|
||||||
|
mockery.enable({
|
||||||
|
useCleanCache: true,
|
||||||
|
// warnOnReplace: false,
|
||||||
|
warnOnUnregistered: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if all offchain oracles are down', async function () {
|
||||||
|
const { GasPriceOracle } = require('../src/index');
|
||||||
|
const oracle = new GasPriceOracle();
|
||||||
|
await oracle.fetchGasPricesOffChain().should.be.rejectedWith('All oracles are down. Probaly network error.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if all onchain oracles are down', async function () {
|
||||||
|
const { GasPriceOracle } = require('../src/index');
|
||||||
|
const oracle = new GasPriceOracle();
|
||||||
|
await oracle.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probaly network error.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if throwIfFailsToFetch is false', async function () {
|
||||||
|
const { GasPriceOracle } = require('../src/index');
|
||||||
|
const oracle = new GasPriceOracle();
|
||||||
|
await oracle.fetchGasPricesOnChain(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if throwIfFailsToFetch is false', async function () {
|
||||||
|
const { GasPriceOracle } = require('../src/index');
|
||||||
|
const oracle = new GasPriceOracle();
|
||||||
|
await oracle.fetchGasPricesOffChain(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
after('after', function () {
|
||||||
|
after(function () {
|
||||||
|
mockery.disable();
|
||||||
|
mockery.deregisterMock('node-fetch');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchGasPricesOnChain', function () {
|
||||||
|
it('should work', async function () {
|
||||||
|
const oracle = new GasPriceOracle();
|
||||||
|
const gas: GasPrice = await oracle.fetchGasPricesOnChain();
|
||||||
|
|
||||||
|
gas.instant.should.be.a('number');
|
||||||
|
gas.fast.should.be.a('number');
|
||||||
|
gas.standard.should.be.a('number');
|
||||||
|
gas.low.should.be.a('number');
|
||||||
|
|
||||||
|
gas.instant.should.be.above(gas.fast);
|
||||||
|
gas.fast.should.be.above(gas.standard);
|
||||||
|
gas.standard.should.be.above(gas.low);
|
||||||
|
gas.low.should.not.be.equal(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
"defaultSeverity": "error",
|
"defaultSeverity": "error",
|
||||||
"rules": {
|
"rules": {
|
||||||
"semicolon": [true, "always"],
|
"semicolon": [true, "always"],
|
||||||
"space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}]
|
"space-before-function-paren": [true, {"anonymous": "always", "named": "never", "asyncArrow": "always"}],
|
||||||
|
"type-literal-delimiter": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user