import { BigNumber , Bytes , ethers , Signer } from "ethers" ; import { BaseProvider , Provider , TransactionRequest } from "@ethersproject/providers" ; import { Event } from 'ethers' ; import { Deferrable , resolveProperties } from "@ethersproject/properties" ; import { SimpleWallet , SimpleWallet__factory , EntryPoint , EntryPoint__factory } from "../typechain" ; import { BytesLike , hexValue } from "@ethersproject/bytes" ; import { TransactionResponse } from "@ethersproject/abstract-provider" ; import { fillAndSign , getRequestId } from "../test/UserOp" ; import { UserOperation } from "../test/UserOperation" ; import { TransactionReceipt } from "@ethersproject/abstract-provider/src.ts/index" ; import { clearInterval } from "timers" ; export type SendUserOp = ( userOp : UserOperation ) => Promise < TransactionResponse | void > ; export let debug = process . env . DEBUG != null ; param provider export function rpcUserOpSender ( provider ) { let chainId ; return async function ( userOp ) { if ( debug ) { console . log ( 'sending' , { ... userOp , initCode : ( userOp . initCode ?? '' ) . length , callData : ( userOp . callData ?? '' ) . length } ) ; } if ( chainId == undefined ) { chainId = await provider . getNetwork ( ) . then ( net => net . chainId ) ; } const cleanUserOp = Object . keys ( userOp ) . map ( key => { let val = ( userOp as any ) [ key ] ; if ( typeof val != 'string' || ! val . startsWith ( '0x' ) ) val = hexValue ( val ) ; return [ key , val ] ; } ) . reduce ( ( set , [ k , v ] ) => ( { ... set , [ k ] : v } ) , { } ) ; await provider . send ( 'eth_sendUserOperation' , [ cleanUserOp ] ) . catch ( e => { throw e . error ?? e ; } ) ; } ; } interface QueueSendUserOp extends SendUserOp { lastQueueUpdate : number ; queueSize : number ; queue : { [ sender ] : UserOperation [ ] ; } ; push : ( ) => Promise < void > ; setInterval : ( intervalMs : number ) => void ; cancelInterval : ( ) => void ; _cancelInterval : any ; } /**
 * a SendUserOp that queue requests. need to call sendQueuedUserOps to create a bundle and send them.
 * the returned object handles the queue of userops and also interval control.
 */ export function queueUserOpSender ( entryPointAddress , signer , intervalMs = 3000 ) { const entryPoint = EntryPoint__factory . connect ( entryPointAddress , signer ) ; let ret = < QueueSendUserOp > async function ( userOp ) { if ( ret . queue [ userOp . sender ] == null ) { ret . queue [ userOp . sender ] = [ ] ; } ret . queue [ userOp . sender ] . push ( userOp ) ; ret . lastQueueUpdate = Date . now ( ) ; ret . queueSize ++ ; } ; ret . queue = { } ; ret . push = async function ( ) { await sendQueuedUserOps ( ret , entryPoint ) ; } ; ret . setInterval = function ( intervalMs ) { ret . cancelInterval ( ) ; ret . _cancelInterval = setInterval ( ret . push , intervalMs ) ; } ; ret . cancelInterval = function ( ) { if ( ret . _cancelInterval != null ) { clearInterval ( ret . _cancelInterval ) ; ret . _cancelInterval = null ; } } ; if ( intervalMs != null ) { ret . setInterval ( intervalMs ) ; } return ret ; } param queue param entryPoint let sending = false ; const IDLE_TIME = 5000 ; const BUNDLE_SIZE_IMMEDIATE = 3 ; async function sendQueuedUserOps ( queueSender , entryPoint ) { if ( sending ) { console . log ( 'sending in progress. waiting' ) ; return ; } sending = true ; try { if ( queueSender . queueSize < BUNDLE_SIZE_IMMEDIATE || queueSender . lastQueueUpdate + IDLE_TIME > Date . now ( ) ) { console . log ( 'queue too small/too young. waiting' ) ; return ; } let ops = [ ] ; const queue = queueSender . queue ; Object . keys ( queue ) . forEach ( sender => { let op = queue [ sender ] . shift ( ) ; if ( op != null ) { ops . push ( op ) ; queueSender . queueSize -- ; } } ) ; if ( ops . length == 0 ) { console . log ( 'no ops to send' ) ; return ; } let signer = await ( entryPoint . provider as any ) . getSigner ( ) . getAddress ( ) ; console . log ( '==== sending batch of ' , ops . length ) ; const ret = await entryPoint . handleOps ( ops , signer , { maxPriorityFeePerGas : 2e9 } ) ; console . log ( 'handleop tx=' , ret . hash ) ; const rcpt = await ret . wait ( ) ; console . log ( 'events=' , rcpt . events ! . map ( e => ( { name : e . event , args : e . args } ) ) ) ; } finally { sending = false ; } } param entryPointAddress param signer param beneficiary export function localUserOpSender ( entryPointAddress , signer , beneficiary ? ) { const entryPoint = EntryPoint__factory . connect ( entryPointAddress , signer ) ; return async function ( userOp ) { if ( debug ) console . log ( 'sending' , { ... userOp , initCode : ( userOp . initCode ?? '' ) . length , callData : ( userOp . callData ?? '' ) . length } ) ; const ret = await entryPoint . handleOps ( [ userOp ] , beneficiary ?? await signer . getAddress ( ) , { gasLimit : 10e6 , maxPriorityFeePerGas : userOp . maxPriorityFeePerGas , maxFeePerGas : userOp . maxFeePerGas } ) ; const rcpt = await ret . wait ( ) ; } ; } export class AAProvider extends BaseProvider { private entryPoint : EntryPoint ; constructor ( entryPointAddress , provider ) { super ( provider . getNetwork ( ) ) ; this . entryPoint = EntryPoint__factory . connect ( entryPointAddress , provider ) ; } } /**
 * a signer that wraps account-abstraction.
 */ export class AASigner extends Signer { _wallet ? : SimpleWallet ; private _isPhantom = true ; public entryPoint : EntryPoint ; private _chainId : Promise < number > | undefined ; param signer param entryPoint param sendUserOp param index constructor ( readonly signer , readonly entryPointAddress , readonly sendUserOp , readonly index = 0 , readonly provider = signer . provider ) { super ( ) ; this . entryPoint = EntryPoint__factory . connect ( entryPointAddress , signer ) ; } async connectWalletAddress ( address ) { if ( this . _wallet != null ) { throw Error ( 'already connected to wallet' ) ; } if ( await this . provider ! . getCode ( address ) . then ( code => code . length ) <= 2 ) { throw new Error ( 'cannot connect to non-existing contract' ) ; } this . _wallet = SimpleWallet__factory . connect ( address , this . signer ) ; this . _isPhantom = false ; } connect ( provider ) { throw new Error ( 'connect not implemented' ) ; } async _deploymentTransaction ( ) { let ownerAddress = await this . signer . getAddress ( ) ; return new SimpleWallet__factory ( ) . getDeployTransaction ( this . entryPoint . address , ownerAddress ) . data ! ; } async getAddress ( ) { await this . syncAccount ( ) ; return this . _wallet ! . address ; } signMessage ( message ) { throw new Error ( 'signMessage: unsupported by AA' ) ; } signTransaction ( transaction ) { throw new Error ( 'signMessage: unsupported by AA' ) ; } async getWallet ( ) { await this . syncAccount ( ) ; return this . _wallet ! ; } async userEventResponse ( userOp ) { const entryPoint = this . entryPoint ; const requestId = getRequestId ( userOp , entryPoint . address , await this . _chainId ! ) ; const provider = entryPoint . provider ; const currentBLock = provider . getBlockNumber ( ) ; let resolved = false ; const waitPromise = new Promise < TransactionReceipt > ( ( resolve , reject ) => { let listener = async function ( this , ... param ) { if ( resolved ) return ; const event = arguments [ arguments . length - 1 ] as Event ; if ( event . blockNumber <= await currentBLock ) { console . log ( 'ignore previous block' , event . blockNumber ) ; return ; } if ( event . args == null ) { console . error ( 'got event without args' , event ) ; return ; } if ( event . args . requestId != requestId ) { console . log ( `== event with wrong requestId: sender/nonce: event.${ event . args . sender }@${ event . args . nonce }!= userOp.${ userOp . sender }@${ parseInt ( userOp . nonce . toString ( ) ) }` ) ; return ; } const rcpt = await event . getTransactionReceipt ( ) ; console . log ( 'got event with status=' , event . args ! . success , 'gasUsed=' , rcpt . gasUsed ) ; if ( ! event . args ! . success ) { console . log ( 'mark tx as failed' ) ; rcpt . status = 0 ; const revertReasonEvents = await entryPoint . queryFilter ( entryPoint . filters . UserOperationRevertReason ( userOp . sender ) , rcpt . blockHash ) ; if ( revertReasonEvents [ 0 ] ) { console . log ( 'rejecting with reason' ) ; reject ( 'UserOp failed with reason: ' + revertReasonEvents [ 0 ] . args . revertReason ) ; return ; } } entryPoint . off ( 'UserOperationEvent' , listener ) ; resolve ( rcpt ) ; resolved = true ; } ; listener = listener . bind ( listener ) ; entryPoint . on ( 'UserOperationEvent' , listener ) ; setTimeout ( ( ) => entryPoint . queryFilter ( entryPoint . filters . UserOperationEvent ( requestId ) ) . then ( query => { if ( query . length > 0 ) { listener ( query [ 0 ] ) ; } } ) , 500 ) ; } ) ; const resp = { hash : requestId , confirmations : 0 , from : userOp . sender , nonce : BigNumber . from ( userOp . nonce ) . toNumber ( ) , gasLimit : BigNumber . from ( userOp . callGas ) , value : BigNumber . from ( 0 ) , data : hexValue ( userOp . callData ) , chainId : await this . _chainId ! , wait : async function ( confirmations ? ) : Promise < TransactionReceipt > { return waitPromise ; } } ; return resp ; } async sendTransaction ( transaction ) { const userOp = await this . _createUserOperation ( transaction ) ; let reponse = await this . userEventResponse ( userOp ) ; await this . sendUserOp ( userOp ) ; return reponse ; } async syncAccount ( ) { if ( ! this . _wallet ) { const address = await this . entryPoint . getSenderAddress ( await this . _deploymentTransaction ( ) , this . index ) ; this . _wallet = SimpleWallet__factory . connect ( address , this . signer ) ; } this . _chainId = this . provider ?. getNetwork ( ) . then ( net => net . chainId ) ; if ( this . _isPhantom ) { const size = await this . signer . provider ?. getCode ( this . _wallet . address ) . then ( x => x . length ) ; this . _isPhantom = size == 2 ; } } async isPhantom ( ) { await this . syncAccount ( ) ; return this . _isPhantom ; } async _createUserOperation ( transaction ) { const tx = await resolveProperties ( transaction ) ; await this . syncAccount ( ) ; let initCode ; if ( this . _isPhantom ) { initCode = await this . _deploymentTransaction ( ) ; } const execFromEntryPoint = await this . _wallet ! . populateTransaction . execFromEntryPoint ( tx . to ! , tx . value ?? 0 , tx . data ! ) ; let { gasPrice , maxPriorityFeePerGas , maxFeePerGas } = tx ; if ( gasPrice ) { maxPriorityFeePerGas = gasPrice ; maxFeePerGas = gasPrice ; } const userOp = await fillAndSign ( { sender : this . _wallet ! . address , initCode , nonce : initCode == null ? tx . nonce : this . index , callData : execFromEntryPoint . data ! , callGas : tx . gasLimit , maxPriorityFeePerGas , maxFeePerGas , } , this . signer , this . entryPoint ) ; return userOp ; } }