From 2e3a5c6cd411569c9515306683b39df6e398a114 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Wed, 1 Jul 2020 18:04:37 +1000 Subject: [PATCH] feat: asset-swapper singleton gas price oracle --- packages/asset-swapper/CHANGELOG.json | 7 ++- packages/asset-swapper/src/swap_quoter.ts | 13 +++-- .../src/utils/protocol_fee_utils.ts | 51 ++++++++++++++----- packages/asset-swapper/test/utils/mocks.ts | 10 +++- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 203e95de32..317bd862b1 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -15,7 +15,12 @@ "pr": 2615 }, { - "note": "Specify EthGasStation url as an optional parameter" + "note": "Specify EthGasStation url as an optional parameter", + "pr": 2617 + }, + { + "note": "Singleton Gas Price Oracle", + "pr": 2619 } ] }, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 7acb0fac77..1d19f91736 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -174,7 +174,7 @@ export class SwapQuoter { : (r => r !== undefined && r.skipBuyRequests === true)(constants.DEFAULT_SWAP_QUOTER_OPTS.rfqt); this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); - this._protocolFeeUtils = new ProtocolFeeUtils( + this._protocolFeeUtils = ProtocolFeeUtils.getInstance( constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, options.ethGasStationUrl, ); @@ -267,7 +267,7 @@ export class SwapQuoter { gasPrice = options.gasPrice; assert.isBigNumber('gasPrice', gasPrice); } else { - gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync(); + gasPrice = await this.getGasPriceEstimationOrThrowAsync(); } const apiOrders = await this.orderbook.getBatchOrdersAsync(makerAssetDatas, [takerAssetData]); @@ -483,6 +483,13 @@ export class SwapQuoter { ]; } + /** + * Returns the recommended gas price for a fast transaction + */ + public async getGasPriceEstimationOrThrowAsync(): Promise { + return this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync(); + } + /** * Destroys any subscriptions or connections. */ @@ -537,7 +544,7 @@ export class SwapQuoter { gasPrice = opts.gasPrice; assert.isBigNumber('gasPrice', gasPrice); } else { - gasPrice = await this._protocolFeeUtils.getGasPriceEstimationOrThrowAsync(); + gasPrice = await this.getGasPriceEstimationOrThrowAsync(); } // get batches of orders from different sources, awaiting sources in parallel const orderBatchPromises: Array> = []; diff --git a/packages/asset-swapper/src/utils/protocol_fee_utils.ts b/packages/asset-swapper/src/utils/protocol_fee_utils.ts index 46631e8648..c7bccb5827 100644 --- a/packages/asset-swapper/src/utils/protocol_fee_utils.ts +++ b/packages/asset-swapper/src/utils/protocol_fee_utils.ts @@ -4,30 +4,38 @@ import * as heartbeats from 'heartbeats'; import { constants } from '../constants'; import { SwapQuoterError } from '../types'; +const MAX_ERROR_COUNT = 5; + export class ProtocolFeeUtils { - public gasPriceEstimation: BigNumber; + private static _instance: ProtocolFeeUtils; + private readonly _ethGasStationUrl!: string; private readonly _gasPriceHeart: any; - private readonly _ethGasStationUrl: string; + private _gasPriceEstimation: BigNumber = constants.ZERO_AMOUNT; + private _errorCount: number = 0; - constructor( + public static getInstance( gasPricePollingIntervalInMs: number, ethGasStationUrl: string = constants.ETH_GAS_STATION_API_URL, initialGasPrice: BigNumber = constants.ZERO_AMOUNT, - ) { - this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs); - this.gasPriceEstimation = initialGasPrice; - this._ethGasStationUrl = ethGasStationUrl; - this._initializeHeartBeat(); + ): ProtocolFeeUtils { + if (!ProtocolFeeUtils._instance) { + ProtocolFeeUtils._instance = new ProtocolFeeUtils( + gasPricePollingIntervalInMs, + ethGasStationUrl, + initialGasPrice, + ); + } + return ProtocolFeeUtils._instance; } public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise { - if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) { + if (this._gasPriceEstimation.eq(constants.ZERO_AMOUNT)) { return this._getGasPriceFromGasStationOrThrowAsync(); } if (shouldHardRefresh) { return this._getGasPriceFromGasStationOrThrowAsync(); } else { - return this.gasPriceEstimation; + return this._gasPriceEstimation; } } @@ -38,6 +46,17 @@ export class ProtocolFeeUtils { this._gasPriceHeart.kill(); } + private constructor( + gasPricePollingIntervalInMs: number, + ethGasStationUrl: string = constants.ETH_GAS_STATION_API_URL, + initialGasPrice: BigNumber = constants.ZERO_AMOUNT, + ) { + this._gasPriceHeart = heartbeats.createHeart(gasPricePollingIntervalInMs); + this._gasPriceEstimation = initialGasPrice; + this._ethGasStationUrl = ethGasStationUrl; + this._initializeHeartBeat(); + } + // tslint:disable-next-line: prefer-function-over-method private async _getGasPriceFromGasStationOrThrowAsync(): Promise { try { @@ -50,15 +69,23 @@ export class ProtocolFeeUtils { // tslint:disable-next-line:custom-no-magic-numbers const unit = new BigNumber(BASE_TEN).pow(9); const gasPriceWei = unit.times(gasPriceGwei); + // Reset the error count to 0 once we have a successful response + this._errorCount = 0; return gasPriceWei; } catch (e) { - throw new Error(SwapQuoterError.NoGasPriceProvidedOrEstimated); + this._errorCount++; + // If we've reached our max error count then throw + if (this._errorCount > MAX_ERROR_COUNT || this._gasPriceEstimation.isZero()) { + this._errorCount = 0; + throw new Error(SwapQuoterError.NoGasPriceProvidedOrEstimated); + } + return this._gasPriceEstimation; } } private _initializeHeartBeat(): void { this._gasPriceHeart.createEvent(1, async () => { - this.gasPriceEstimation = await this._getGasPriceFromGasStationOrThrowAsync(); + this._gasPriceEstimation = await this._getGasPriceFromGasStationOrThrowAsync(); }); } } diff --git a/packages/asset-swapper/test/utils/mocks.ts b/packages/asset-swapper/test/utils/mocks.ts index 109c4b7d09..25161cde33 100644 --- a/packages/asset-swapper/test/utils/mocks.ts +++ b/packages/asset-swapper/test/utils/mocks.ts @@ -54,7 +54,13 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde return mockedSwapQuoter; }; -class ProtocolFeeUtilsClass extends ProtocolFeeUtils { +class ProtocolFeeUtilsClass { + public static getInstance(..._args: any[]): any { + return { + getGasPriceEstimationOrThrowAsync: async () => + Promise.resolve(new BigNumber(devConstants.DEFAULT_GAS_PRICE)), + }; + } // tslint:disable-next-line:prefer-function-over-method public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise { return new BigNumber(devConstants.DEFAULT_GAS_PRICE); @@ -64,7 +70,7 @@ class ProtocolFeeUtilsClass extends ProtocolFeeUtils { export const protocolFeeUtilsMock = (): TypeMoq.IMock => { const mockProtocolFeeUtils = TypeMoq.Mock.ofType(ProtocolFeeUtilsClass, TypeMoq.MockBehavior.Loose); mockProtocolFeeUtils.callBase = true; - return mockProtocolFeeUtils; + return mockProtocolFeeUtils as any; }; const mockGetSignedOrdersWithFillableAmountsAsyncAsync = (