import WalletConnect from "@walletconnect/client" ; import { KeyRingStore , PermissionStore } from "@keplr-wallet/stores" ; import { action , autorun , computed , makeObservable , observable , runInAction , } from "mobx" ; import { ChainStore } from "../chain" ; import { Keplr } from "@keplr-wallet/provider" ; import { Buffer } from "buffer/" ; import { KVStore } from "@keplr-wallet/common" ; import { WCMessageRequester } from "./msg-requester" ; import { RNRouterBackground } from "../../router" ; import { getBasicAccessPermissionType , KeyRingStatus , } from "@keplr-wallet/background" ; import { computedFn } from "mobx-utils" ; import { Key } from "@keplr-wallet/types" ; import { AppState , Linking } from "react-native" ; export interface WalletConnectV1SessionRequest { id : number ; jsonrpc : "2.0" ; method : string ; params : [ { peerId : string ; peerMeta ? : { name ? : string ; description ? : string ; icons ? : string [ ] ; url ? : string ; } ; } ] ; } export interface SessionRequestApproval { key : string ; peerMeta : WalletConnectV1SessionRequest [ "params" ] [ 0 ] [ "peerMeta" ] ; params : [ { chains : string [ ] ; } ] ; resolve : ( ) => void ; reject : ( ) => void ; } export abstract class WalletConnectManager { @ observable . shallow protected clientMap : Map < string , WalletConnect > = new Map ( ) ; @ observable . shallow protected pendingClientMap : Map < string , WalletConnect > = new Map ( ) ; protected constructor ( protected readonly chainStore , protected readonly keyRingStore ) { makeObservable ( this ) ; } hasClient ( uri ) { return this . clientMap . has ( uri ) ; } getClient ( uri ) { return this . clientMap . get ( uri ) ; } async restoreClient ( session ) { const client = new WalletConnect ( { clientMeta : { name : "Keplr" , description : "Wallet for interchain" , url : "#" , icons : [ "https://dhj8dql1kzq2v.cloudfront.net/keplr-256x256.png" ] , } , session , } ) ; if ( client . connected ) { this . onSessionConnected ( client ) ; client . on ( "call_request" , ( error , payload ) => { this . onCallRequest ( client , error , payload ) ; } ) ; client . on ( "disconnect" , ( error ) => { if ( error ) { console . log ( error ) ; return ; } this . onSessionDisconnected ( client ) ; } ) ; } else { this . onSessionDisconnected ( client ) ; } } protected async waitInitStores ( ) { if ( this . chainStore . isInitializing ) { await new Promise < void > ( ( resolve ) => { const disposer = autorun ( ( ) => { if ( ! this . chainStore . isInitializing ) { resolve ( ) ; if ( disposer ) { disposer ( ) ; } } } ) ; } ) ; } if ( this . keyRingStore . status !== KeyRingStatus . UNLOCKED ) { await new Promise < void > ( ( resolve ) => { const disposer = autorun ( ( ) => { if ( this . keyRingStore . status === KeyRingStatus . UNLOCKED ) { resolve ( ) ; if ( disposer ) { disposer ( ) ; } } } ) ; } ) ; } } isClientInitialized ( uri ) { return this . clientMap . has ( uri ) ; } isClientPending ( uri ) { return this . pendingClientMap . has ( uri ) ; } canInitClient ( uri ) { return ! this . isClientInitialized ( uri ) && ! this . isClientPending ( uri ) ; } async initClient ( uri ) { await this . waitInitStores ( ) ; if ( this . clientMap . has ( uri ) ) { throw new Error ( "Client already initialized" ) ; } if ( this . pendingClientMap . has ( uri ) ) { throw new Error ( "Client is waiting session" ) ; } const client = new WalletConnect ( { uri , clientMeta : { name : "Keplr" , description : "Wallet for interchain" , url : "#" , icons : [ "https://dhj8dql1kzq2v.cloudfront.net/keplr-256x256.png" ] , } , } ) ; runInAction ( ( ) => { this . pendingClientMap . set ( uri , client ) ; } ) ; let resolver ; let rejector ; const promise = new Promise < void > ( ( resolve , reject ) => { resolver = resolve ; rejector = reject ; } ) ; const timeout = setTimeout ( ( ) => { rejector ( new Error ( "Timeout" ) ) ; } , 10000 ) ; const handler = ( error ) => { if ( error ) { console . log ( error ) ; return ; } client . on ( "disconnect" , ( error ) => { if ( error ) { console . log ( error ) ; return ; } this . onSessionDisconnected ( client ) ; } ) ; if ( ! client . peerMeta ?. url ) { client . rejectSession ( { message : "Should provide the peer url" , } ) ; resolver ( ) ; return ; } client . approveSession ( { chainId : 99999 , accounts : [ ] , } ) ; this . onSessionConnected ( client ) ; resolver ( ) ; } ; client . on ( "session_request" , handler ) ; try { await promise ; client . off ( "session_request" ) ; runInAction ( ( ) => { this . clientMap . set ( uri , client ) ; } ) ; client . on ( "call_request" , ( error , payload ) => { this . onCallRequest ( client , error , payload ) ; } ) ; return client ; } finally { runInAction ( ( ) => { this . pendingClientMap . delete ( uri ) ; } ) ; clearTimeout ( timeout ) ; } } protected createKeplrAPI ( sessionId ) { return new Keplr ( "" , "core" , new WCMessageRequester ( RNRouterBackground . EventEmitter , sessionId ) ) ; } protected readonly onCallRequest = async ( client , error , payload ) => { if ( error ) { console . log ( error ) ; return ; } const id = payload . id ; if ( ! id ) { console . log ( "Payload's id is empty" , payload ) ; return ; } await this . waitInitStores ( ) ; const keplr = this . createKeplrAPI ( client . session . key ) ; try { this . onCallBeforeRequested ( client ) ; switch ( payload . method ) { case "keplr_enable_wallet_connect_v1" : { if ( payload . params . length === 0 ) { throw new Error ( "Invalid parmas" ) ; } for ( const param of payload . params ) { if ( typeof param !== "string" ) { throw new Error ( "Invalid parmas" ) ; } } await keplr . enable ( payload . params ) ; client . approveRequest ( { id , result : [ ] , } ) ; break ; } case "keplr_get_key_wallet_connect_v1" : { if ( payload . params . length !== 1 ) { throw new Error ( "Invalid parmas" ) ; } if ( typeof payload . params [ 0 ] !== "string" ) { throw new Error ( "Invalid parmas" ) ; } const key = await keplr . getKey ( payload . params [ 0 ] ) ; client . approveRequest ( { id , result : [ { name : key . name , algo : key . algo , pubKey : Buffer . from ( key . pubKey ) . toString ( "hex" ) , address : Buffer . from ( key . address ) . toString ( "hex" ) , bech32Address : key . bech32Address , isNanoLedger : key . isNanoLedger , } , ] , } ) ; break ; } case "keplr_sign_amino_wallet_connect_v1" : { if ( payload . params . length !== 3 && payload . params . length !== 4 ) { throw new Error ( "Invalid parmas" ) ; } const result = await keplr . signAmino ( payload . params [ 0 ] , payload . params [ 1 ] , payload . params [ 2 ] , payload . params [ 3 ] ) ; client . approveRequest ( { id , result : [ result ] , } ) ; break ; } default : throw new Error ( `Unknown method (${ payload . method })` ) ; } } catch ( e ) { client . rejectRequest ( { id , error : { message : e . message , } , } ) ; } finally { this . onCallAfterRequested ( client ) ; } } ; protected abstract onSessionConnected ( client ) ; protected abstract onSessionDisconnected ( client ) ; protected onCallBeforeRequested ( _ ) { } protected onCallAfterRequested ( _ ) { } } export class WalletConnectStore extends WalletConnectManager { @ observable . shallow protected _clients : WalletConnect [ ] = [ ] ; @ observable protected _isPendingClientFromDeepLink : boolean = false ; @ observable protected _needGoBackToBrowser : boolean = false ; protected _isAndroidActivityKilled = false ; protected wcCallCount : number = 0 ; protected isPendingWcCallFromDeepLinkClient = false ; protected deepLinkClientKeyMap : Record < string , true | undefined > = { } ; constructor ( protected readonly kvStore , protected readonly eventListener , protected readonly chainStore , protected readonly keyRingStore , protected readonly permissionStore ) { super ( chainStore , keyRingStore ) ; makeObservable ( this ) ; this . restore ( ) ; this . initDeepLink ( ) ; this . eventListener . addEventListener ( "keplr_keystoreunlock" , ( ) => this . sendAccountMayChangedEventToClients ( ) ) ; this . eventListener . addEventListener ( "keplr_keystorechange" , ( ) => this . sendAccountMayChangedEventToClients ( ) ) ; } get isPendingClientFromDeepLink ( ) : boolean { return this . _isPendingClientFromDeepLink ; } /**
     needGoBackToBrowser indicates that all requests from the wallet connect are processed when the request is from the deep link.
     This store doesn't show any indicator to user or close the app.
     The other component (maybe provider) should act according to this field.
     */ get needGoBackToBrowser ( ) : boolean { return this . _needGoBackToBrowser ; } protected async initDeepLink ( ) { await this . checkInitialURL ( ) ; Linking . addEventListener ( "url" , ( e ) => { this . processDeepLinkURL ( e . url ) ; } ) ; AppState . addEventListener ( "change" , ( state ) => { if ( state === "active" ) { if ( this . _isAndroidActivityKilled ) { this . checkInitialURL ( ) ; } this . _isAndroidActivityKilled = false ; } else { this . clearNeedGoBackToBrowser ( ) ; } } ) ; } protected async checkInitialURL ( ) { const initialURL = await Linking . getInitialURL ( ) ; if ( initialURL ) { this . processDeepLinkURL ( initialURL ) ; } } protected processDeepLinkURL ( _url ) { try { const url = new URL ( _url ) ; if ( url . protocol === "keplrwallet:" && url . host === "wcV1" ) { let params = url . search ; if ( params ) { if ( params . startsWith ( "?" ) ) { params = params . slice ( 1 ) ; } if ( this . canInitClient ( params ) ) { runInAction ( ( ) => { this . _isPendingClientFromDeepLink = true ; } ) ; this . initClient ( params ) . then ( ( client ) => { this . deepLinkClientKeyMap [ client . key ] = true ; this . saveDeepLinkClientKeyMap ( this . deepLinkClientKeyMap ) ; } ) . finally ( ( ) => { runInAction ( ( ) => { this . _isPendingClientFromDeepLink = false ; } ) ; } ) ; } } } } catch ( e ) { console . log ( e ) ; } } onAndroidActivityKilled ( ) { this . _isAndroidActivityKilled = true ; } protected onCallBeforeRequested ( client ) { super . onCallBeforeRequested ( client ) ; this . wcCallCount ++ ; if ( this . deepLinkClientKeyMap [ client . session . key ] ) { this . isPendingWcCallFromDeepLinkClient = true ; } } @ action protected onCallAfterRequested ( client ) { super . onCallAfterRequested ( client ) ; this . wcCallCount -- ; if ( this . wcCallCount == 0 && this . isPendingWcCallFromDeepLinkClient ) { this . isPendingWcCallFromDeepLinkClient = false ; if ( AppState . currentState === "active" ) { this . _needGoBackToBrowser = true ; } } } /**
     clearNeedGoBackToBrowser is used in the component to set the needGoBackToBrowser as false.
     */ @ action clearNeedGoBackToBrowser ( ) { this . _needGoBackToBrowser = false ; } protected async sendAccountMayChangedEventToClients ( ) { await this . waitInitStores ( ) ; const keyForChainCache = { } ; for ( const client of this . _clients ) { let keys ; const keplr = this . createKeplrAPI ( client . session . key ) ; const permittedChains = await this . permissionStore . getOriginPermittedChains ( WCMessageRequester . getVirtualSessionURL ( client . session . key ) , getBasicAccessPermissionType ( ) ) ; for ( const chain of permittedChains ) { const key = keyForChainCache [ chain ] ?? ( await keplr . getKey ( chain ) ) ; if ( ! keyForChainCache [ chain ] ) { keyForChainCache [ chain ] = key ; } if ( ! keys ) { keys = { name : key . name , algo : key . algo , isNanoLedger : key . isNanoLedger , keys : [ { chainIdentifier : chain , pubKey : Buffer . from ( key . pubKey ) . toString ( "hex" ) , address : Buffer . from ( key . address ) . toString ( "hex" ) , bech32Address : key . bech32Address , } , ] , } ; } else { keys . keys . push ( { chainIdentifier : chain , pubKey : Buffer . from ( key . pubKey ) . toString ( "hex" ) , address : Buffer . from ( key . address ) . toString ( "hex" ) , bech32Address : key . bech32Address , } ) ; } } if ( keys ) { client . sendCustomRequest ( { id : Math . floor ( Math . random ( ) * 100000 ) , jsonrpc : "2.0" , method : "keplr_keystore_may_changed_event_wallet_connect_v1" , params : [ keys ] , } ) ; } } } getSession = computedFn ( ( sessionId ) => { return this . sessions . find ( ( session ) => session . key === sessionId ) ; } ) ; @ computed get sessions ( ) : WalletConnect [ "session" ] [ ] { return this . _clients . map ( ( client ) => { return client . session ; } ) ; } protected async restore ( ) { const deepLinkClientMap = await this . kvStore . get < Record < string , true | undefined > > ( "deep_link_client_keys" ) ; if ( deepLinkClientMap ) { this . deepLinkClientKeyMap = deepLinkClientMap ; } const persistentSessions = await this . getPersistentSessions ( ) ; for ( const session of persistentSessions ) { this . restoreClient ( session ) ; } } async disconnect ( sessionId ) { const client = this . _clients . find ( ( client ) => client . session . key === sessionId ) ; if ( client ) { await client . killSession ( { message : "User requests disconnection" , } ) ; } } protected async getPersistentSessions ( ) { const result = await this . kvStore . get < WalletConnect [ "session" ] [ ] > ( "persistent_session_v1" ) ; if ( ! result ) { return [ ] ; } return result ; } protected async setPersistentSessions ( value ) { await this . kvStore . set ( "persistent_session_v1" , value ) ; } protected async saveDeepLinkClientKeyMap ( keys ) { this . deepLinkClientKeyMap = keys ; await this . kvStore . set ( "deep_link_client_keys" , keys ) ; } protected async onSessionConnected ( client ) { const clients = this . _clients ; if ( ! clients . find ( ( persistent ) => persistent . session . key === client . session . key ) ) { runInAction ( ( ) => { clients . push ( client ) ; } ) ; await this . setPersistentSessions ( clients . map ( ( client ) => { return client . session ; } ) ) ; } } protected async onSessionDisconnected ( client ) { if ( this . deepLinkClientKeyMap [ client . session . key ] ) { delete this . deepLinkClientKeyMap [ client . session . key ] ; await this . saveDeepLinkClientKeyMap ( this . deepLinkClientKeyMap ) ; } runInAction ( ( ) => { this . _clients = this . _clients . filter ( ( persistent ) => persistent . session . key !== client . session . key ) ; } ) ; await this . setPersistentSessions ( this . _clients . map ( ( client ) => { return client . session ; } ) ) ; for ( const chainInfo of this . chainStore . chainInfos ) { await this . permissionStore . getBasicAccessInfo ( chainInfo . chainId ) . removeOrigin ( WCMessageRequester . getVirtualSessionURL ( client . session . key ) ) ; } } }