import { first } from 'lodash-unified' ; import type { TransactionReceipt } from 'web3-core' ; import type { JsonRpcPayload } from 'web3-core-helpers' ; import { WalletMessages } from '@masknet/plugin-wallet' ; import { Explorer } from '@masknet/web3-providers' ; import { ChainId , getExplorerConstants , isSameAddress , TransactionStateType } from '@masknet/web3-shared-evm' ; import * as EthereumService from '../../../../extension/background-script/EthereumService' ; import * as progress from './progress' ; import * as helpers from './helpers' ; import { currentChainIdSettings } from '../../settings' ; import { WalletRPC } from '../../messages' ; interface StorageItem { at : number ; limits : number ; payload : JsonRpcPayload ; receipt : Promise < TransactionReceipt | null > | null ; } class Storage { static MAX_ITEM_SIZE = 40 ; private map = new Map < ChainId , Map < string , StorageItem > > ( ) ; private getStorage ( chainId ) { if ( ! this . map . has ( chainId ) ) this . map . set ( chainId , new Map ( ) ) ; return this . map . get ( chainId ) ! ; } public hasItem ( chainId , hash ) { return this . getStorage ( chainId ) . has ( hash ) ; } public getItem ( chainId , hash ) { return this . getStorage ( chainId ) . get ( hash ) ; } public setItem ( chainId , hash , transaction ) { this . getStorage ( chainId ) . set ( hash , transaction ) ; } public removeItem ( chainId , hash ) { this . getStorage ( chainId ) . delete ( hash ) ; } public getItems ( chainId ) { const map = this . getStorage ( chainId ) ; return map ? [ ... map . entries ( ) ] . sort ( ( [ , a ] , [ , z ] ) => z . at - a . at ) : [ ] ; } public getWatched ( chainId ) { return this . getItems ( chainId ) . slice ( 0 , Storage . MAX_ITEM_SIZE ) ; } public getUnwatched ( chainId ) { return this . getItems ( chainId ) . slice ( Storage . MAX_ITEM_SIZE ) ; } public getWatchedAccounts ( chainId ) { return this . getWatched ( chainId ) . map ( ( [ _ , transaction ] ) => helpers . getPayloadFrom ( transaction . payload ) ) . filter ( Boolean ) as string [ ] ; } public getUnwatchedAccounts ( chainId ) { return this . getUnwatched ( chainId ) . map ( ( [ _ , transaction ] ) => helpers . getPayloadFrom ( transaction . payload ) ) . filter ( Boolean ) as string [ ] ; } } let timer = null ; const storage = new Storage ( ) ; const CHECK_TIMES = 30 ; const CHECK_DELAY = 30 * 1000 ; const CHECK_LATEST_SIZE = 5 ; async function getTransactionReceipt ( chainId , hash ) { try { const transaction = await EthereumService . getTransactionByHash ( hash , { chainId , } ) ; if ( ! transaction ) return null ; progress . notifyTransactionProgress ( transaction , { type : TransactionStateType . HASH , hash , } ) ; const receipt = await EthereumService . getTransactionReceipt ( hash , { chainId , } ) ; if ( ! receipt ) return null ; const transactionState = helpers . getTransactionState ( receipt ) ; progress . notifyTransactionProgress ( transaction , transactionState ) ; WalletMessages . events . transactionStateUpdated . sendToAll ( transactionState ) ; return receipt ; } catch { return null ; } } async function checkReceipt ( chainId ) { await Promise . allSettled ( storage . getWatched ( chainId ) . map ( async ( [ hash , transaction ] ) => { const receipt = await storage . getItem ( chainId , hash ) ?. receipt ; if ( receipt ) return ; storage . setItem ( chainId , hash , { ... transaction , receipt : getTransactionReceipt ( chainId , hash ) , } ) ; } ) ) ; } async function checkAccount ( chainId , account ) { const { API_KEYS = [ ] , EXPLORER_API = '' } = getExplorerConstants ( chainId ) ; const watchedTransactions = storage . getWatched ( chainId ) ; const latestTransactions = await Explorer . getLatestTransactions ( account , EXPLORER_API , { offset : CHECK_LATEST_SIZE , apikey : first ( API_KEYS ) , } ) ; for ( const latestTransaction of latestTransactions ) { const [ watchedHash , watchedTransaction ] = watchedTransactions . find ( ( [ hash , transaction ] ) => { if ( latestTransaction . hash === hash ) return true ; if ( ! transaction . payload ) return false ; if ( helpers . getTransactionId ( latestTransaction ) === helpers . getPayloadId ( transaction . payload ) ) return true ; const config = helpers . getPayloadConfig ( transaction . payload ) ; if ( ! config ) return false ; return ( isSameAddress ( latestTransaction . from , config . from as string ) && latestTransaction . nonce === config . nonce ) ; } ) ?? [ ] ; if ( ! watchedHash || ! watchedTransaction ?. payload || watchedHash === latestTransaction . hash ) continue ; await WalletRPC . replaceRecentTransaction ( chainId , account , watchedHash , latestTransaction . hash , watchedTransaction . payload ) ; storage . removeItem ( chainId , watchedHash ) ; storage . setItem ( chainId , latestTransaction . hash , { ... watchedTransaction , payload : helpers . toPayload ( latestTransaction ) , receipt : getTransactionReceipt ( chainId , latestTransaction . hash ) , } ) ; } } async function check ( ) { stopCheck ( ) ; const chainId = currentChainIdSettings . value ; storage . getUnwatched ( chainId ) . forEach ( ( [ hash ] ) => unwatchTransaction ( chainId , hash ) ) ; storage . getWatched ( chainId ) . forEach ( ( [ hash , transaction ] ) => { storage . setItem ( chainId , hash , { ... transaction , limits : Math . max ( 0 , transaction . limits - 1 ) , } ) ; } ) ; try { await checkReceipt ( chainId ) ; for ( const account of storage . getWatchedAccounts ( chainId ) ) await checkAccount ( chainId , account ) ; } catch ( error ) { } const allSettled = await Promise . allSettled ( storage . getWatched ( chainId ) . map ( ( [ , transaction ] ) => transaction . receipt ) ) ; if ( allSettled . every ( ( x ) => x . status === 'fulfilled' && x . value ) ) return ; startCheck ( true ) ; } function startCheck ( force ) { if ( force ) stopCheck ( ) ; if ( timer === null ) { timer = setTimeout ( check , CHECK_DELAY ) ; } } function stopCheck ( ) { if ( timer !== null ) clearTimeout ( timer ) ; timer = null ; } export async function getReceipt ( chainId , hash ) { return storage . getItem ( chainId , hash ) ?. receipt ?? null ; } export async function watchTransaction ( chainId , hash , payload ) { if ( ! storage . hasItem ( chainId , hash ) ) { storage . setItem ( chainId , hash , { at : Date . now ( ) , payload , limits : CHECK_TIMES , receipt : Promise . resolve ( null ) , } ) ; } startCheck ( false ) ; } export function unwatchTransaction ( chainId , hash ) { storage . removeItem ( chainId , hash ) ; }