From d59f275c6e2d2aaebe3e8fc73576ef6cf91bd20f Mon Sep 17 00:00:00 2001 From: guanbinrui Date: Tue, 12 Jul 2022 17:25:19 +0800 Subject: [PATCH] fix: nonce confliction --- .../src/state/Connection/middleware/Nonce.ts | 140 ++++-------------- 1 file changed, 29 insertions(+), 111 deletions(-) diff --git a/packages/plugins/EVM/src/state/Connection/middleware/Nonce.ts b/packages/plugins/EVM/src/state/Connection/middleware/Nonce.ts index 4dd6945f0d6a..b5bfae20b392 100644 --- a/packages/plugins/EVM/src/state/Connection/middleware/Nonce.ts +++ b/packages/plugins/EVM/src/state/Connection/middleware/Nonce.ts @@ -1,120 +1,38 @@ import { EthereumAddress } from 'wallet.ts' +import Web3 from 'web3' import { toHex } from 'web3-utils' -import { EthereumMethodType, ProviderType } from '@masknet/web3-shared-evm' -import type { Context, EVM_Connection, Middleware } from '../types' -import { SharedContextSettings } from '../../../settings' - -class NonceManager { - constructor(private address: string, private connection: EVM_Connection) {} - - private nonce = NonceManager.INITIAL_NONCE - private locked = false - private tasks: Array<() => void> = [] - - private lock() { - this.locked = true - } - private unlock() { - this.locked = false - } - private continue() { - if (!this.locked) this.tasks.shift()?.() - } - private async getRemoteNonce() { - const { chainId } = SharedContextSettings.value - - return new Promise(async (resolve, reject) => { - const callback = (error: Error | null, nonce = 0) => { - if (error) reject(error) - else resolve(nonce) - this.unlock() - this.continue() - } - const run = async () => { - try { - this.lock() - callback( - null, - await this.connection.getTransactionNonce(this.address, { - chainId: chainId.getCurrentValue(), - // Only mask wallets need to use Nonce - providerType: ProviderType.MaskWallet, - }), - ) - } catch (error: unknown) { - callback(error instanceof Error ? error : new Error('Failed to get remote nonce.')) - } - } - if (this.locked) this.tasks.push(run) - else run() - }) - } - - public async getNonce() { - const nonce = Math.max(await this.getRemoteNonce(), this.nonce) - await this.setNonce(nonce) - return nonce - } - - public async setNonce(nonce: number) { - return new Promise(async (resolve, reject) => { - const callback = (e: Error | null) => { - if (e) reject(e) - else resolve() - this.unlock() - this.continue() - } - const run = async () => { - this.lock() - this.nonce = nonce - callback(null) - } - if (this.locked) this.tasks.push(run) - else run() - }) - } - - public async resetNonce() { - const nonce = await this.getRemoteNonce() - await this.setNonce(nonce) - } - - static INITIAL_NONCE = -1 -} +import { first } from 'lodash-unified' +import { ChainId, EthereumMethodType, getRPCConstants, ProviderType } from '@masknet/web3-shared-evm' +import type { Context, Middleware } from '../types' export class Nonce implements Middleware { - private cache = new Map() - - constructor() { - // const { chainId } = SharedContextSettings.value - // // reset all nonce if the chain id of mask wallet was changed - // chainId.subscribe(() => { - // this.resetAllNonce() - // }) - } + static INITIAL_NONCE = -1 - private getManager(address: string, connection: EVM_Connection) { - const address_ = EthereumAddress.checksumAddress(address) - if (!this.cache.has(address_)) this.cache.set(address_, new NonceManager(address_, connection)) - return this.cache.get(address_)! - } + private nonces = new Map>() - private getNonce(address: string, connection: EVM_Connection) { - return this.getManager(address, connection).getNonce() - } + private getRemoteNonce(chainId: ChainId, address: string) { + const RPC_URL = first(getRPCConstants(chainId).RPC_URLS) + if (!RPC_URL) throw new Error('Failed to fetch nonce.') - private async commitNonce(address: string, connection: EVM_Connection) { - const manager = this.getManager(address, connection) - return manager.setNonce((await manager.getNonce()) + 1) + const web3 = new Web3(RPC_URL) + return web3.eth.getTransactionCount(address) } - private resetNonce(address: string, connection: EVM_Connection) { - const manager = this.getManager(address, connection) - return manager.resetNonce() - } - - private async resetAllNonce() { - await Promise.all(Array.from(this.cache.values()).map((m) => m.resetNonce())) + private async syncRemoteNonce(chainId: ChainId, address: string, commitment = 0) { + const address_ = EthereumAddress.checksumAddress(address) + const addressNonces = this.nonces.get(address_) ?? new Map() + addressNonces.set( + chainId, + commitment + + Math.max( + await this.getRemoteNonce(chainId, address), + addressNonces.get(chainId) ?? Nonce.INITIAL_NONCE, + ), + ) + + // set back into cache + this.nonces.set(address_, addressNonces) + return addressNonces.get(chainId)! } async fn(context: Context, next: () => Promise) { @@ -129,7 +47,7 @@ export class Nonce implements Middleware { params: [ { ...context.config, - nonce: toHex(await this.getNonce(context.account, context.connection)), + nonce: toHex(await this.syncRemoteNonce(context.chainId, context.account)), }, ], } @@ -145,10 +63,10 @@ export class Nonce implements Middleware { const isAuroraErrorNonce = message.includes('ERR_INCORRECT_NONCE') // if a transaction hash was received then commit the nonce - if (isGeneralErrorNonce || isAuroraErrorNonce) await this.resetNonce(context.account, context.connection) + if (isGeneralErrorNonce || isAuroraErrorNonce) await this.syncRemoteNonce(context.chainId, context.account) // else if a nonce error was occurred then reset the nonce else if (!context.error && typeof context.result === 'string') - await this.commitNonce(context.account, context.connection) + await this.syncRemoteNonce(context.chainId, context.account, 1) } catch { // to scan the context to determine how to update the local nonce, allow to fail silently }