import { Duplex } from 'stream' ; import pump from 'pump' ; import { JsonRpcEngine , createIdRemapMiddleware , JsonRpcRequest , JsonRpcId , JsonRpcVersion , JsonRpcSuccess , JsonRpcMiddleware , } from 'json-rpc-engine' ; import { createStreamMiddleware } from 'json-rpc-middleware-stream' ; import ObjectMultiplex from '@metamask/object-multiplex' ; import SafeEventEmitter from '@metamask/safe-event-emitter' ; import dequal from 'fast-deep-equal' ; import { ethErrors , EthereumRpcError } from 'eth-rpc-errors' ; import { duplex as isDuplex } from 'is-stream' ; import messages from './messages' ; import { createErrorMiddleware , EMITTED_NOTIFICATIONS , getRpcPromiseCallback , logStreamDisconnectWarning , ConsoleLike , Maybe , } from './utils' ; export interface UnvalidatedJsonRpcRequest { id ? : JsonRpcId ; jsonrpc ? : JsonRpcVersion ; method : string ; params ? : unknown ; } export interface BaseProviderOptions { /**
     * The name of the stream used to connect to the wallet.
     */ jsonRpcStreamName ? : string ; /**
     * The logging API to use.
     */ logger ? : ConsoleLike ; /**
     * The maximum number of event listeners.
     */ maxEventListeners ? : number ; } export interface RequestArguments { /** The RPC method to request. */ method : string ; /** The params of the RPC method, if any. */ params ? : unknown [ ] | Record < string , unknown > ; } export interface BaseProviderState { accounts : null | string [ ] ; isConnected : boolean ; isUnlocked : boolean ; initialized : boolean ; isPermanentlyDisconnected : boolean ; } export interface JsonRpcConnection { events : SafeEventEmitter ; middleware : JsonRpcMiddleware < unknown , unknown > ; stream : Duplex ; } export default class BaseProvider extends SafeEventEmitter { protected readonly _log : ConsoleLike ; protected _state : BaseProviderState ; protected _rpcEngine : JsonRpcEngine ; protected _jsonRpcConnection : JsonRpcConnection ; protected static _defaultState : BaseProviderState = { accounts : null , isConnected : false , isUnlocked : false , initialized : false , isPermanentlyDisconnected : false , } ; /**
     * The chain ID of the currently connected Ethereum chain.
     * See [chainId.network] https for more information.
      public chainId : string | null ; /**
     * The user's currently selected Ethereum address.
     * If null, MetaMask is either locked or the user has not permitted any
     * addresses to be viewed.
     */ public selectedAddress : string | null ; param connectionStream param options param options . jsonRpcStreamName param options . logger param options . maxEventListeners constructor ( connectionStream , { jsonRpcStreamName = 'metamask-provider' , logger = console , maxEventListeners = 100 , } = { } ) { super ( ) ; if ( ! isDuplex ( connectionStream ) ) { throw new Error ( messages . errors . invalidDuplexStream ( ) ) ; } this . _log = logger ; this . setMaxListeners ( maxEventListeners ) ; this . _state = { ... BaseProvider . _defaultState , } ; this . selectedAddress = null ; this . chainId = null ; this . _handleAccountsChanged = this . _handleAccountsChanged . bind ( this ) ; this . _handleConnect = this . _handleConnect . bind ( this ) ; this . _handleChainChanged = this . _handleChainChanged . bind ( this ) ; this . _handleDisconnect = this . _handleDisconnect . bind ( this ) ; this . _handleStreamDisconnect = this . _handleStreamDisconnect . bind ( this ) ; this . _handleUnlockStateChanged = this . _handleUnlockStateChanged . bind ( this ) ; this . _rpcRequest = this . _rpcRequest . bind ( this ) ; this . request = this . request . bind ( this ) ; const mux = new ObjectMultiplex ( ) ; pump ( connectionStream , mux as unknown as Duplex , connectionStream , this . _handleStreamDisconnect . bind ( this , 'MetaMask' ) ) ; this . on ( 'connect' , ( ) => { this . _state . isConnected = true ; } ) ; this . _jsonRpcConnection = createStreamMiddleware ( ) ; pump ( this . _jsonRpcConnection . stream , mux . createStream ( jsonRpcStreamName ) as unknown as Duplex , this . _jsonRpcConnection . stream , this . _handleStreamDisconnect . bind ( this , 'MetaMask RpcProvider' ) ) ; const rpcEngine = new JsonRpcEngine ( ) ; rpcEngine . push ( createIdRemapMiddleware ( ) ) ; rpcEngine . push ( createErrorMiddleware ( this . _log ) ) ; rpcEngine . push ( this . _jsonRpcConnection . middleware ) ; this . _rpcEngine = rpcEngine ; this . _initializeState ( ) ; this . _jsonRpcConnection . events . on ( 'notification' , ( payload ) => { const { method , params } = payload ; if ( method === 'metamask_accountsChanged' ) { this . _handleAccountsChanged ( params ) ; } else if ( method === 'metamask_unlockStateChanged' ) { this . _handleUnlockStateChanged ( params ) ; } else if ( method === 'metamask_chainChanged' ) { this . _handleChainChanged ( params ) ; } else if ( EMITTED_NOTIFICATIONS . includes ( method ) ) { this . emit ( 'message' , { type : method , data : params , } ) ; } else if ( method === 'METAMASK_STREAM_FAILURE' ) { connectionStream . destroy ( new Error ( messages . errors . permanentlyDisconnected ( ) ) ) ; } } ) ; } /**
     * Returns whether the provider can process RPC requests.
     */ isConnected ( ) { return this . _state . isConnected ; } param args param args . method param args . params returns async request < T > ( args ) { if ( ! args || typeof args !== 'object' || Array . isArray ( args ) ) { throw ethErrors . rpc . invalidRequest ( { message : messages . errors . invalidRequestArgs ( ) , data : args , } ) ; } const { method , params } = args ; if ( typeof method !== 'string' || method . length === 0 ) { throw ethErrors . rpc . invalidRequest ( { message : messages . errors . invalidRequestMethod ( ) , data : args , } ) ; } if ( params !== undefined && ! Array . isArray ( params ) && ( typeof params !== 'object' || params === null ) ) { throw ethErrors . rpc . invalidRequest ( { message : messages . errors . invalidRequestParams ( ) , data : args , } ) ; } return new Promise < T > ( ( resolve , reject ) => { this . _rpcRequest ( { method , params } , getRpcPromiseCallback ( resolve , reject ) ) ; } ) ; } /**
     * Constructor helper.
     * Populates initial state by calling 'metamask_getProviderState' and emits
     * necessary events.
     */ private async _initializeState ( ) { try { const { accounts , chainId , isUnlocked , networkVersion } = ( await this . request ( { method : 'metamask_getProviderState' , } ) ) as { accounts : string [ ] ; chainId : string ; isUnlocked : boolean ; networkVersion : string ; } ; this . emit ( 'connect' , { chainId } ) ; this . _handleChainChanged ( { chainId , networkVersion } ) ; this . _handleUnlockStateChanged ( { accounts , isUnlocked } ) ; this . _handleAccountsChanged ( accounts ) ; } catch ( error ) { this . _log . error ( 'MetaMask: Failed to get initial state. Please report this bug.' , error ) ; } finally { this . _state . initialized = true ; this . emit ( '_initialized' ) ; } } param payload param callback protected _rpcRequest ( payload , callback ) { let cb = callback ; if ( ! Array . isArray ( payload ) ) { if ( ! payload . jsonrpc ) { payload . jsonrpc = '2.0' ; } if ( payload . method === 'eth_accounts' || payload . method === 'eth_requestAccounts' ) { cb = ( err , res ) => { this . _handleAccountsChanged ( res . result || [ ] , payload . method === 'eth_accounts' ) ; callback ( err , res ) ; } ; } return this . _rpcEngine . handle ( payload as JsonRpcRequest < unknown > , cb ) ; } return this . _rpcEngine . handle ( payload as JsonRpcRequest < unknown > [ ] , cb ) ; } param chainId emits protected _handleConnect ( chainId ) { if ( ! this . _state . isConnected ) { this . _state . isConnected = true ; this . emit ( 'connect' , { chainId } ) ; this . _log . debug ( messages . info . connected ( chainId ) ) ; } } param isRecoverable param errorMessage emits protected _handleDisconnect ( isRecoverable , errorMessage ? ) { if ( this . _state . isConnected || ( ! this . _state . isPermanentlyDisconnected && ! isRecoverable ) ) { this . _state . isConnected = false ; let error ; if ( isRecoverable ) { error = new EthereumRpcError ( 1013 , errorMessage || messages . errors . disconnected ( ) ) ; this . _log . debug ( error ) ; } else { error = new EthereumRpcError ( 1011 , errorMessage || messages . errors . permanentlyDisconnected ( ) ) ; this . _log . error ( error ) ; this . chainId = null ; this . _state . accounts = null ; this . selectedAddress = null ; this . _state . isUnlocked = false ; this . _state . isPermanentlyDisconnected = true ; } this . emit ( 'disconnect' , error ) ; } } emits protected _handleStreamDisconnect ( streamName , error ) { logStreamDisconnectWarning ( this . _log , streamName , error , this ) ; this . _handleDisconnect ( false , error ? error . message : undefined ) ; } emits param networkInfo param networkInfo . chainId param networkInfo . networkVersion protected _handleChainChanged ( { chainId , networkVersion , } = { } ) { if ( ! chainId || typeof chainId !== 'string' || ! chainId . startsWith ( '0x' ) || ! networkVersion || typeof networkVersion !== 'string' ) { this . _log . error ( 'MetaMask: Received invalid network parameters. Please report this bug.' , { chainId , networkVersion } ) ; return ; } if ( networkVersion === 'loading' ) { this . _handleDisconnect ( true ) ; } else { this . _handleConnect ( chainId ) ; if ( chainId !== this . chainId ) { this . chainId = chainId ; if ( this . _state . initialized ) { this . emit ( 'chainChanged' , this . chainId ) ; } } } } param accounts param isEthAccounts protected _handleAccountsChanged ( accounts , isEthAccounts = false ) { let _accounts = accounts ; if ( ! Array . isArray ( accounts ) ) { this . _log . error ( 'MetaMask: Received invalid accounts parameter. Please report this bug.' , accounts ) ; _accounts = [ ] ; } for ( const account of accounts ) { if ( typeof account !== 'string' ) { this . _log . error ( 'MetaMask: Received non-string account. Please report this bug.' , accounts ) ; _accounts = [ ] ; break ; } } if ( ! dequal ( this . _state . accounts , _accounts ) ) { if ( isEthAccounts && this . _state . accounts !== null ) { this . _log . error ( `MetaMask: 'eth_accounts' unexpectedly updated accounts. Please report this bug.` , _accounts ) ; } this . _state . accounts = _accounts as string [ ] ; if ( this . selectedAddress !== _accounts [ 0 ] ) { this . selectedAddress = ( _accounts [ 0 ] as string ) || null ; } if ( this . _state . initialized ) { this . emit ( 'accountsChanged' , _accounts ) ; } } } param opts param opts . accounts param opts . isUnlocked protected _handleUnlockStateChanged ( { accounts , isUnlocked , } = { } ) { if ( typeof isUnlocked !== 'boolean' ) { this . _log . error ( 'MetaMask: Received invalid isUnlocked parameter. Please report this bug.' ) ; return ; } if ( isUnlocked !== this . _state . isUnlocked ) { this . _state . isUnlocked = isUnlocked ; this . _handleAccountsChanged ( accounts || [ ] ) ; } } }