Skip to content

Commit 484f680

Browse files
authored
Merge pull request #70 from quocle108/cache-quote-fee
Cache quote fee
2 parents b234c90 + 0163f33 commit 484f680

7 files changed

Lines changed: 251 additions & 87 deletions

package-lock.json

Lines changed: 5 additions & 78 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"dependencies": {
3232
"@tetherto/wdk-safe-relay-kit": "4.1.5",
3333
"@tetherto/wdk-wallet": "1.0.0-beta.7",
34-
"@tetherto/wdk-wallet-evm": "1.0.0-beta.8",
34+
"@tetherto/wdk-wallet-evm": "1.0.0-beta.10",
3535
"bare-node-runtime": "^1.1.4",
3636
"ethers": "6.14.4"
3737
},

src/wallet-account-evm-erc-4337.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import WalletAccountReadOnlyEvmErc4337 from './wallet-account-read-only-evm-erc-
4040

4141
/** @typedef {import('@tetherto/wdk-safe-relay-kit').Safe4337Pack} Safe4337Pack */
4242

43-
const FEE_TOLERANCE_COEFFICIENT = 120n
43+
const QUOTE_MAX_AGE_MS = 2 * 60 * 1_000
4444

4545
const USDT_MAINNET_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
4646

@@ -169,9 +169,10 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc
169169

170170
const { isSponsored, useNativeCoins } = mergedConfig
171171

172-
const { fee } = await this.quoteSendTransaction(tx, config)
172+
const fee = this._getValidCachedFee(tx) ?? (await this.quoteSendTransaction(tx, config)).fee
173+
this._lastQuote = undefined
173174

174-
const amountToApprove = (isSponsored || useNativeCoins) ? 0n : BigInt(fee * FEE_TOLERANCE_COEFFICIENT / 100n)
175+
const amountToApprove = (isSponsored || useNativeCoins) ? 0n : fee
175176

176177
const hash = await this._sendUserOperation([tx].flat(), {
177178
...mergedConfig,
@@ -199,13 +200,14 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc
199200

200201
const tx = await WalletAccountEvm._getTransferTransaction(options)
201202

202-
const { fee } = await this.quoteSendTransaction(tx, config)
203+
const fee = this._getValidCachedFee(tx) ?? (await this.quoteSendTransaction(tx, config)).fee
204+
this._lastQuote = undefined
203205

204206
if (!isSponsored && transferMaxFee !== undefined && fee >= transferMaxFee) {
205207
throw new Error('Exceeded maximum fee cost for transfer operation.')
206208
}
207209

208-
const amountToApprove = (isSponsored || useNativeCoins) ? 0n : BigInt(fee * FEE_TOLERANCE_COEFFICIENT / 100n)
210+
const amountToApprove = (isSponsored || useNativeCoins) ? 0n : fee
209211

210212
const hash = await this._sendUserOperation([tx], {
211213
...mergedConfig,
@@ -255,6 +257,35 @@ export default class WalletAccountEvmErc4337 extends WalletAccountReadOnlyEvmErc
255257
return safe4337Pack
256258
}
257259

260+
/**
261+
* Returns the cached fee if it exists, is not expired, and matches the given transaction.
262+
* Clears cache on match or expiry; preserves it on mismatch.
263+
*
264+
* @private
265+
* @param {EvmTransaction | EvmTransaction[]} tx - The transaction to match against.
266+
* @returns {bigint | undefined} The cached fee, or undefined if not available, expired, or mismatched.
267+
*/
268+
_getValidCachedFee (tx) {
269+
const quote = this._lastQuote
270+
271+
if (!quote) {
272+
return undefined
273+
}
274+
275+
if (Date.now() - quote.createdAt > QUOTE_MAX_AGE_MS) {
276+
this._lastQuote = undefined
277+
return undefined
278+
}
279+
280+
if (WalletAccountReadOnlyEvmErc4337._getTxKey(tx) !== quote.txKey) {
281+
return undefined
282+
}
283+
284+
this._lastQuote = undefined
285+
286+
return quote.fee
287+
}
288+
258289
/** @private */
259290
async _sendUserOperation (txs, { amountToApprove, ...config }) {
260291
const { useNativeCoins, paymasterToken, isSponsored, sponsorshipPolicyId } = config

src/wallet-account-read-only-evm-erc-4337.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { Safe4337Pack, GenericFeeEstimator, PimlicoFeeEstimator } from '@tethert
2222

2323
import { ConfigurationError } from './errors.js'
2424

25+
const FEE_TOLERANCE_COEFFICIENT = 120n
26+
2527
/** @typedef {import('ethers').Eip1193Provider} Eip1193Provider */
2628

2729
/** @typedef {import('@tetherto/wdk-safe-relay-kit').UserOperationReceipt} UserOperationReceipt */
@@ -37,6 +39,13 @@ import { ConfigurationError } from './errors.js'
3739

3840
/** @typedef {import('@tetherto/wdk-wallet-evm').TypedData} TypedData */
3941

42+
/**
43+
* @typedef {Object} CachedQuote
44+
* @property {bigint} fee - The estimated fee with tolerance buffer applied.
45+
* @property {number} createdAt - The timestamp when the quote was created.
46+
* @property {string} txKey - A serialized key of the transaction used for cache matching.
47+
*/
48+
4049
/**
4150
* @typedef {Object} EvmErc4337WalletCommonConfig
4251
* @property {number} chainId - The blockchain's id (e.g., 1 for ethereum).
@@ -114,6 +123,14 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
114123
*/
115124
this._feeEstimator = undefined
116125

126+
/**
127+
* Cached quote from the last fee estimation.
128+
*
129+
* @protected
130+
* @type {CachedQuote | undefined}
131+
*/
132+
this._lastQuote = undefined
133+
117134
/**
118135
* The chain id.
119136
*
@@ -199,6 +216,9 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
199216
/**
200217
* Quotes the costs of a send transaction operation.
201218
*
219+
* The result is cached internally for up to 2 minutes. If `sendTransaction` is called with the
220+
* same transaction within that window, the cached fee is reused without an additional RPC round-trip.
221+
*
202222
* @param {EvmTransaction | EvmTransaction[]} tx - The transaction, or an array of multiple transactions to send in batch.
203223
* @param {Partial<EvmErc4337WalletPaymasterTokenConfig | EvmErc4337WalletSponsorshipPolicyConfig | EvmErc4337WalletNativeCoinsConfig>} [config] - If set, overrides the given configuration options.
204224
* @returns {Promise<Omit<TransactionResult, 'hash'>>} The transaction's quotes.
@@ -216,17 +236,24 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
216236
return { fee: 0n }
217237
}
218238

219-
const fee = await this._getUserOperationGasCost([tx].flat(), {
239+
const estimatedFee = await this._getUserOperationGasCost([tx].flat(), {
220240
...mergedConfig,
221241
amountToApprove: useNativeCoins ? 0n : BigInt(Number.MAX_SAFE_INTEGER)
222242
})
223243

224-
return { fee: BigInt(fee) }
244+
const fee = BigInt(estimatedFee) * FEE_TOLERANCE_COEFFICIENT / 100n
245+
246+
this._lastQuote = { fee, createdAt: Date.now(), txKey: WalletAccountReadOnlyEvmErc4337._getTxKey(tx) }
247+
248+
return { fee }
225249
}
226250

227251
/**
228252
* Quotes the costs of a transfer operation.
229253
*
254+
* The result is cached internally for up to 2 minutes. If `transfer` is called with the
255+
* same transaction within that window, the cached fee is reused without an additional RPC round-trip.
256+
*
230257
* @param {TransferOptions} options - The transfer's options.
231258
* @param {Partial<EvmErc4337WalletPaymasterTokenConfig | EvmErc4337WalletSponsorshipPolicyConfig | EvmErc4337WalletNativeCoinsConfig>} [config] - If set, overrides the given configuration options.
232259
* @returns {Promise<Omit<TransferResult, 'hash'>>} The transfer's quotes.
@@ -448,6 +475,17 @@ export default class WalletAccountReadOnlyEvmErc4337 extends WalletAccountReadOn
448475
return this._feeEstimator
449476
}
450477

478+
/**
479+
* Returns a serialized key for transaction cache matching.
480+
*
481+
* @protected
482+
* @param {EvmTransaction | EvmTransaction[]} tx - The transaction(s) to serialize.
483+
* @returns {string} The serialized transaction key.
484+
*/
485+
static _getTxKey (tx) {
486+
return JSON.stringify([tx].flat(), (_, v) => typeof v === 'bigint' ? v.toString() : v)
487+
}
488+
451489
/** @private */
452490
async _getUserOperationGasCost (txs, { amountToApprove, ...config }) {
453491
const safe4337Pack = await this._getSafe4337Pack(config)

0 commit comments

Comments
 (0)