diff --git a/zp-relayer/configs/relayerConfig.ts b/zp-relayer/configs/relayerConfig.ts index 08cbbcc..ac24da9 100644 --- a/zp-relayer/configs/relayerConfig.ts +++ b/zp-relayer/configs/relayerConfig.ts @@ -65,6 +65,7 @@ const config = { treeProverType: (process.env.RELAYER_TREE_PROVER_TYPE || ProverType.Local) as ProverType, directDepositProverType: (process.env.RELAYER_DD_PROVER_TYPE || ProverType.Local) as ProverType, feeManagerType: (process.env.RELAYER_FEE_MANAGER_TYPE || FeeManagerType.Dynamic) as FeeManagerType, + feeManagerUpdateInterval: parseInt(process.env.RELAYER_FEE_MANAGER_UPDATE_INTERVAL || '10000'), feeMarginFactor: toBN(process.env.RELAYER_FEE_MARGIN_FACTOR || '100'), feeScalingFactor: toBN(process.env.RELAYER_FEE_SCALING_FACTOR || '100'), priceFeedType: (process.env.RELAYER_PRICE_FEED_TYPE || PriceFeedType.Native) as PriceFeedType, diff --git a/zp-relayer/endpoints.ts b/zp-relayer/endpoints.ts index 442afec..a6a9514 100644 --- a/zp-relayer/endpoints.ts +++ b/zp-relayer/endpoints.ts @@ -210,7 +210,7 @@ function getFeeBuilder(feeManager: FeeManager) { return async (req: Request, res: Response) => { validateBatch([[checkTraceId, req.headers]]) - const feeOptions = await feeManager.getFees({ gasLimit: config.relayerGasLimit }) + const feeOptions = await feeManager.getFeeOptions({ gasLimit: config.relayerGasLimit }) const fees = feeOptions.denominate(pool.denominator).getObject() res.json(fees) diff --git a/zp-relayer/init.ts b/zp-relayer/init.ts index e45b48c..8f853e8 100644 --- a/zp-relayer/init.ts +++ b/zp-relayer/init.ts @@ -41,6 +41,8 @@ function buildFeeManager( priceFeed, scaleFactor: config.feeScalingFactor, marginFactor: config.feeMarginFactor, + updateInterval: config.feeManagerUpdateInterval, + defaultFeeOptionsParams: { gasLimit: config.relayerGasLimit }, } if (type === FeeManagerType.Static) { if (config.relayerFee === null) throw new Error('Static relayer fee is not set') @@ -105,7 +107,7 @@ export async function init() { const priceFeed = buildPriceFeed(config.priceFeedType, web3) const feeManager = buildFeeManager(config.feeManagerType, priceFeed, gasPriceService, web3) - await feeManager.init() + await feeManager.start() const workerPromises = [ createPoolTxWorker({ diff --git a/zp-relayer/services/fee/DynamicFeeManager.ts b/zp-relayer/services/fee/DynamicFeeManager.ts index d94b7a9..3270bd7 100644 --- a/zp-relayer/services/fee/DynamicFeeManager.ts +++ b/zp-relayer/services/fee/DynamicFeeManager.ts @@ -21,7 +21,7 @@ export class DynamicFeeManager extends FeeManager { return new FeeEstimate(toBN(fee)) } - async _getFees({ gasLimit }: IGetFeesParams) { + async _fetchFeeOptions({ gasLimit }: IGetFeesParams) { const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit) return new DefaultUserFeeOptions(baseFee) } diff --git a/zp-relayer/services/fee/FeeManager.ts b/zp-relayer/services/fee/FeeManager.ts index 8b143be..77739f4 100644 --- a/zp-relayer/services/fee/FeeManager.ts +++ b/zp-relayer/services/fee/FeeManager.ts @@ -2,6 +2,8 @@ import type BN from 'bn.js' import { toBN } from 'web3-utils' import type { IPriceFeed } from '../price-feed/IPriceFeed' import { GasPrice, EstimationType, getMaxRequiredGasPrice } from '../gas-price' +import { setIntervalAndRun } from '@/utils/helpers' +import { logger } from '../appLogger' export interface IGetFeesParams { gasLimit: BN @@ -53,12 +55,32 @@ export interface IFeeManagerConfig { priceFeed: IPriceFeed scaleFactor: BN marginFactor: BN + updateInterval: number + defaultFeeOptionsParams: IGetFeesParams } export abstract class FeeManager { + private cachedFeeOptions: IUserFeeOptions | null = null + private updateFeeOptionsInterval: NodeJS.Timeout | null = null + constructor(protected config: IFeeManagerConfig) {} - abstract init(): Promise + protected abstract init(): Promise + + async start() { + await this.init() + + if (this.updateFeeOptionsInterval) clearInterval(this.updateFeeOptionsInterval) + + this.updateFeeOptionsInterval = await setIntervalAndRun(async () => { + const feeOptions = await this.fetchFeeOptions(this.config.defaultFeeOptionsParams) + logger.debug('Updating cached fee options', { + old: this.cachedFeeOptions?.getObject(), + new: feeOptions.getObject(), + }) + this.cachedFeeOptions = feeOptions + }, this.config.updateInterval) + } static async estimateExecutionFee(gasPrice: GasPrice, gasLimit: BN): Promise { const price = await gasPrice.fetchOnce() @@ -72,21 +94,37 @@ export abstract class FeeManager { } async estimateFee(params: IFeeEstimateParams): Promise { - const fees = await this.getFees(params) + const fees = await this.getFeeOptions(params, false) const estimatedFee = await this._estimateFee(params, fees) const marginedFee = estimatedFee.applyFactor(this.config.marginFactor) return marginedFee } - async getFees(params: IGetFeesParams): Promise { - const feeOptions = await this._getFees(params) + async fetchFeeOptions(params: IGetFeesParams): Promise { + const feeOptions = await this._fetchFeeOptions(params) const convertedFees = await this.convertAndScale(feeOptions) + return convertedFees } + async getFeeOptions(params: IGetFeesParams, useCached = true): Promise { + if (useCached && this.cachedFeeOptions) return this.cachedFeeOptions + let feeOptions: IUserFeeOptions + try { + feeOptions = await this.fetchFeeOptions(params) + logger.debug('Fetched fee options', feeOptions.getObject()) + } catch (e) { + logger.error('Failed to fetch fee options', e) + if (!this.cachedFeeOptions) throw e + logger.debug('Fallback to cache fee options') + feeOptions = this.cachedFeeOptions + } + return feeOptions + } + // Should be used for tx fee validation protected abstract _estimateFee(params: IFeeEstimateParams, fees: IUserFeeOptions): Promise // Should provide fee estimations for users - protected abstract _getFees(params: IGetFeesParams): Promise + protected abstract _fetchFeeOptions(params: IGetFeesParams): Promise } diff --git a/zp-relayer/services/fee/OptimismFeeManager.ts b/zp-relayer/services/fee/OptimismFeeManager.ts index 7330d56..b64ffef 100644 --- a/zp-relayer/services/fee/OptimismFeeManager.ts +++ b/zp-relayer/services/fee/OptimismFeeManager.ts @@ -43,7 +43,7 @@ class OptimismUserFeeOptions implements IUserFeeOptions { getObject() { return { - baseFee: this.baseFee.toString(10), + fee: this.baseFee.toString(10), oneByteFee: this.oneByteFee.toString(10), } } @@ -84,7 +84,7 @@ export class OptimismFeeManager extends FeeManager { } async _estimateFee({ extraData }: IFeeEstimateParams, feeOptions: OptimismUserFeeOptions) { - const { baseFee, oneByteFee } = feeOptions.getObject() + const { fee: baseFee, oneByteFee } = feeOptions.getObject() const unscaledL1Fee = this.getL1Fee(MOCK_CALLDATA + extraData, toBN(oneByteFee)) @@ -92,15 +92,14 @@ export class OptimismFeeManager extends FeeManager { // We do it here to get a more accurate result const l1Fee = unscaledL1Fee.divn(NZERO_BYTE_GAS) - const fee = toBN(baseFee).add(l1Fee) + const feeEstimate = toBN(baseFee).add(l1Fee) - return new FeeEstimate(fee) + return new FeeEstimate(feeEstimate) } - async _getFees({ gasLimit }: IGetFeesParams): Promise { + async _fetchFeeOptions({ gasLimit }: IGetFeesParams): Promise { const baseFee = await FeeManager.estimateExecutionFee(this.gasPrice, gasLimit) - // TODO: cache const l1BaseFee = await contractCallRetry(this.oracle, 'l1BaseFee').then(toBN) // Use an upper bound for the oneByteFee const oneByteFee = l1BaseFee.muln(NZERO_BYTE_GAS) diff --git a/zp-relayer/services/fee/StaticFeeManager.ts b/zp-relayer/services/fee/StaticFeeManager.ts index 3630f93..c908eb9 100644 --- a/zp-relayer/services/fee/StaticFeeManager.ts +++ b/zp-relayer/services/fee/StaticFeeManager.ts @@ -12,7 +12,7 @@ export class StaticFeeManager extends FeeManager { return new FeeEstimate(this.staticFee) } - async _getFees() { + async _fetchFeeOptions() { return new DefaultUserFeeOptions(this.staticFee) } } diff --git a/zp-relayer/test/worker-tests/poolWorker.test.ts b/zp-relayer/test/worker-tests/poolWorker.test.ts index 9d8980c..3487555 100644 --- a/zp-relayer/test/worker-tests/poolWorker.test.ts +++ b/zp-relayer/test/worker-tests/poolWorker.test.ts @@ -39,7 +39,7 @@ import flowZeroAddressWithdraw from '../flows/flow_zero-address_withdraw_2.json' import { Params } from 'libzkbob-rs-node' import { directDepositQueue } from '../../queue/directDepositQueue' import { createDirectDepositWorker } from '../../workers/directDepositWorker' -import { FeeManager, DefaultFeeManager } from '../../services/fee' +import { DynamicFeeManager, FeeManager } from '../../services/fee' chai.use(chaiAsPromised) const expect = chai.expect @@ -106,9 +106,11 @@ describe('poolWorker', () => { priceFeed: mockPriceFeed, scaleFactor: toBN(1), marginFactor: toBN(1), + updateInterval: config.feeManagerUpdateInterval, + defaultFeeOptionsParams: { gasLimit: config.relayerGasLimit }, } - feeManager = new DefaultFeeManager(managerConfig) - await feeManager.init() + feeManager = new DynamicFeeManager(managerConfig, gasPriceService) + await feeManager.start() txManager = new TxManager(web3, config.relayerPrivateKey, gasPriceService) await txManager.init()