onchain oracle support

This commit is contained in:
Alexey 2020-06-01 17:29:58 +03:00
parent beea051c8a
commit 2bcfa0003d
7 changed files with 214 additions and 23 deletions

26
package-lock.json generated

@ -42,6 +42,12 @@
"integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==",
"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": {
"version": "14.0.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.5.tgz",
@ -118,6 +124,11 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
@ -180,6 +191,15 @@
"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": {
"version": "2.4.2",
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",

@ -7,7 +7,7 @@
"prepare": "npm run build",
"prepublishOnly": "npm run lint",
"scripts": {
"test": "mocha -r ts-node/register tests/*.test.ts",
"test": "mocha --timeout 30000 -r ts-node/register tests/*.test.ts",
"build": "tsc",
"lint": "tslint -p tsconfig.json"
},
@ -16,14 +16,18 @@
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/mockery": "^1.4.29",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"mocha": "^7.2.0",
"mockery": "^2.1.0",
"tslint": "^6.1.2",
"tslint-config-standard": "^9.0.0"
},
"dependencies": {
"@types/node": "^14.0.5",
"@types/node-fetch": "^2.5.7",
"bignumber.js": "^9.0.0",
"node-fetch": "^2.6.0",
"ts-node": "^8.10.1",
"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',
url: 'https://ethgasstation.info/json/ethgasAPI.json',
instantPropertyName: 'fastest',
@ -11,7 +11,7 @@ const ethgasstation: Oracle = {
denominator: 10
};
const zoltu: Oracle = {
const zoltu: OffChainOracle = {
name: 'zoltu',
url: 'https://gas-oracle.zoltu.io/',
instantPropertyName: 'percentile_99',
@ -21,8 +21,18 @@ const zoltu: Oracle = {
denominator: 1
};
const chainLink: OnChainOracle = {
name: 'chainLink',
callData: '0x50d25bcd',
contract: '0xA417221ef64b1549575C977764E651c9FAB50141',
denominator: '1000000000'
};
export default {
oracles: [
offChainOracles: [
ethgasstation, zoltu
],
onChainOracles: [
chainLink
]
};

@ -1,6 +1,7 @@
import fetch from 'node-fetch';
import config from './config';
import { GasPrice, Oracle } from './types';
import { GasPrice, OffChainOracle, OnChainOracle } from './types';
import BigNumber from 'bignumber.js';
export class GasPriceOracle {
lastGasPrice: GasPrice = {
@ -9,9 +10,16 @@ export class GasPriceOracle {
standard: 10,
low: 1
};
defaultRpc = 'https://api.mycryptoapi.com/eth';
async fetchGasPrices(): Promise<GasPrice> {
for (let oracle of config.oracles) {
constructor(defaultRpc?: string) {
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;
try {
const response = await fetch(url);
@ -27,6 +35,7 @@ export class GasPriceOracle {
low: parseFloat(gas[lowPropertyName]) / denominator
};
this.lastGasPrice = gasPrices;
return this.lastGasPrice;
} else {
throw new Error(`Fetch gasPrice from ${name} oracle failed. Trying another one...`);
}
@ -34,11 +43,63 @@ export class GasPriceOracle {
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;
}
addOracle(oracle: Oracle) {
config.oracles.push(oracle);
async fetchGasPricesOnChain(throwIfFailsToFetch = true): Promise<GasPrice> {
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);
}
}

@ -1,11 +1,19 @@
export type Oracle = {
name: string
url: string
instantPropertyName: string,
fastPropertyName: string,
standardPropertyName: string,
lowPropertyName: string,
denominator: number
export type OffChainOracle = {
name: string;
url: string;
instantPropertyName: string;
fastPropertyName: string;
standardPropertyName: string;
lowPropertyName: string;
denominator: number;
};
export type OnChainOracle = {
name: string;
rpc?: string;
contract: string;
callData: string;
denominator: string;
};
export type GasPrice = {

@ -1,10 +1,91 @@
import { GasPriceOracle } from '../src/index';
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 () {
const oracle = new GasPriceOracle();
const gas: GasPrice = await oracle.fetchGasPrices();
console.log(gas);
const gas: GasPrice = await oracle.fetchGasPricesOffChain();
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",
"rules": {
"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
}
}