import { Inject , Injectable , Optional } from '@angular/core' ; import { ComponentStore } from '@ngrx/component-store' ; import { Adapter , SendTransactionOptions , Wallet , WalletName , WalletNotConnectedError , WalletNotReadyError , } from '@solana/wallet-adapter-base' ; import { Connection , PublicKey , Transaction } from '@solana/web3.js' ; import { combineLatest , defer , EMPTY , from , Observable , of , Subject , throwError } from 'rxjs' ; import { catchError , concatMap , filter , finalize , first , map , switchMap , tap , withLatestFrom } from 'rxjs/operators' ; import { fromAdapterEvent , isNotNull } from '../operators' ; import { LocalStorageService } from './local-storage' ; import { WalletNotSelectedError } from './wallet.errors' ; import { signAllTransactions , signMessage , signTransaction } from './wallet.signer' ; import { WALLET_CONFIG } from './wallet.tokens' ; import { WalletConfig , WalletState } from './wallet.types' ; export const WALLET_DEFAULT_CONFIG = { wallets : [ ] , autoConnect : false , localStorageKey : 'walletName' , } ; const initialState = { wallet : null , adapter : null , ready : false , connected : false , publicKey : null , } ; @ Injectable ( ) export class WalletStore extends ComponentStore < WalletState > { private readonly _error = new Subject ( ) ; private readonly _localStorage = new LocalStorageService < WalletName | null > ( this . _config ?. localStorageKey || 'walletName' , null ) ; readonly wallets$ = this . select ( ( state ) => state . wallets ) ; readonly autoConnect$ = this . select ( ( state ) => state . autoConnect ) ; readonly wallet$ = this . select ( ( state ) => state . wallet ) ; readonly adapter$ = this . select ( ( state ) => state . adapter ) ; readonly publicKey$ = this . select ( ( state ) => state . publicKey ) ; readonly ready$ = this . select ( ( state ) => state . ready ) ; readonly connecting$ = this . select ( ( state ) => state . connecting ) ; readonly disconnecting$ = this . select ( ( state ) => state . disconnecting ) ; readonly connected$ = this . select ( ( state ) => state . connected ) ; readonly name$ = this . select ( ( state ) => state . name ) ; readonly error$ = this . _error . asObservable ( ) ; readonly anchorWallet$ = this . select ( this . publicKey$ , this . adapter$ , this . connected$ , ( publicKey , adapter , connected ) => { const adapterSignTransaction = adapter && 'signTransaction' in adapter ? signTransaction ( adapter , connected , this . _error ) : undefined ; const adapterSignAllTransactions = adapter && 'signAllTransactions' in adapter ? signAllTransactions ( adapter , connected , this . _error ) : undefined ; return publicKey && adapterSignTransaction && adapterSignAllTransactions ? { publicKey , signTransaction : ( transaction ) => adapterSignTransaction ( transaction ) . toPromise ( ) , signAllTransactions : ( transactions ) => adapterSignAllTransactions ( transactions ) . toPromise ( ) , } : undefined ; } , { debounce : true } ) ; private readonly _walletsByName$ = this . select ( this . wallets$ , ( wallets ) => wallets . reduce < Record < WalletName , Wallet > > ( ( walletsByName , wallet ) => { walletsByName [ wallet . name ] = wallet ; return walletsByName ; } , { } ) ) ; constructor ( @ Optional ( ) @ Inject ( WALLET_CONFIG ) private _config ) { super ( ) ; this . _config = { ... WALLET_DEFAULT_CONFIG , ... this . _config , } ; this . setState ( { ... initialState , wallets : this . _config ?. wallets || [ ] , name : this . _localStorage . value , connecting : false , disconnecting : false , autoConnect : this . _config ?. autoConnect || false , } ) ; } readonly onWalletChanged = this . effect ( ( ) => combineLatest ( [ this . name$ , this . _walletsByName$ ] ) . pipe ( tap ( ( [ name , walletsByName ] ) => { const wallet = ( name && walletsByName [ name ] ) || null ; const adapter = wallet && wallet . adapter ; if ( adapter ) { const { publicKey , connected } = adapter ; this . patchState ( { name , adapter , wallet , publicKey , connected , ready : false , } ) ; } else { this . patchState ( initialState ) ; } } ) ) ) ; readonly autoConnect = this . effect ( ( ) => { return combineLatest ( [ this . autoConnect$ , this . adapter$ . pipe ( isNotNull ) , this . ready$ , this . connecting$ , this . connected$ , ] ) . pipe ( filter ( ( [ autoConnect , , ready , connecting , connected ] ) => autoConnect && ready && ! connecting && ! connected ) , concatMap ( ( [ , adapter ] ) => { this . patchState ( { connecting : true } ) ; return from ( defer ( ( ) => adapter . connect ( ) ) ) . pipe ( catchError ( ( ) => { this . patchState ( { name : null } ) ; return of ( null ) ; } ) , finalize ( ( ) => this . patchState ( { connecting : false } ) ) ) ; } ) ) ; } ) ; readonly selectWallet = this . effect ( ( newName$ ) => { return newName$ . pipe ( concatMap ( ( action ) => of ( action ) . pipe ( withLatestFrom ( this . name$ , this . adapter$ ) ) ) , filter ( ( [ newName , name ] ) => newName !== name ) , concatMap ( ( [ newName , , adapter ] ) => { if ( ! adapter ) { return of ( newName ) ; } else { return from ( defer ( ( ) => adapter . disconnect ( ) ) ) . pipe ( map ( ( ) => newName ) ) ; } } ) , tap ( ( newName ) => { this . _localStorage . setItem ( newName ) ; this . patchState ( { name : newName } ) ; } ) ) ; } ) ; readonly onConnect = this . effect ( ( ) => { return this . adapter$ . pipe ( isNotNull , switchMap ( ( adapter ) => fromAdapterEvent ( adapter , 'connect' ) . pipe ( tap ( ( ) => { const { connected , publicKey } = adapter ; this . patchState ( { connected , publicKey , } ) ; } ) ) ) ) ; } ) ; readonly onDisconnect = this . effect ( ( ) => { return this . adapter$ . pipe ( isNotNull , switchMap ( ( adapter ) => fromAdapterEvent ( adapter , 'disconnect' ) . pipe ( tap ( ( ) => this . patchState ( { name : null } ) ) ) ) ) ; } ) ; readonly onError = this . effect ( ( ) => { return this . adapter$ . pipe ( isNotNull , switchMap ( ( adapter ) => fromAdapterEvent ( adapter , 'error' ) . pipe ( tap ( ( error ) => this . _error . next ( error ) ) ) ) ) ; } ) ; connect ( ) { return combineLatest ( [ this . connecting$ , this . disconnecting$ , this . connected$ , this . wallet$ , this . adapter$ , this . ready$ , ] ) . pipe ( first ( ) , filter ( ( [ connecting , disconnecting , connected ] ) => ! connected && ! connecting && ! disconnecting ) , concatMap ( ( [ , , , wallet , adapter , ready ] ) => { if ( ! wallet || ! adapter ) { const error = new WalletNotSelectedError ( ) ; this . _error . next ( error ) ; return throwError ( error ) ; } if ( ! ready ) { this . patchState ( { name : null } ) ; if ( typeof window !== 'undefined' ) { window . open ( wallet . url , '_blank' ) ; } const error = new WalletNotReadyError ( ) ; this . _error . next ( error ) ; return throwError ( error ) ; } this . patchState ( { connecting : true } ) ; return from ( defer ( ( ) => adapter . connect ( ) ) ) . pipe ( catchError ( ( error ) => { this . patchState ( { name : null } ) ; return throwError ( error ) ; } ) , finalize ( ( ) => this . patchState ( { connecting : false } ) ) ) ; } ) ) ; } disconnect ( ) { return combineLatest ( [ this . disconnecting$ , this . adapter$ ] ) . pipe ( first ( ) , filter ( ( [ disconnecting ] ) => ! disconnecting ) , concatMap ( ( [ , adapter ] ) => { if ( ! adapter ) { this . patchState ( { name : null } ) ; return EMPTY ; } else { this . patchState ( { disconnecting : true } ) ; return from ( defer ( ( ) => adapter . disconnect ( ) ) ) . pipe ( finalize ( ( ) => this . patchState ( { disconnecting : false , name : null } ) ) ) ; } } ) ) ; } sendTransaction ( transaction , connection , options ? ) { return combineLatest ( [ this . adapter$ , this . connected$ ] ) . pipe ( first ( ) , concatMap ( ( [ adapter , connected ] ) => { if ( ! adapter ) { const error = new WalletNotSelectedError ( ) ; this . _error . next ( error ) ; return throwError ( error ) ; } if ( ! connected ) { const error = new WalletNotConnectedError ( ) ; this . _error . next ( error ) ; return throwError ( error ) ; } return from ( defer ( ( ) => adapter . sendTransaction ( transaction , connection , options ) ) ) ; } ) ) ; } signTransaction ( transaction ) { const { adapter , connected } = this . get ( ) ; return adapter && 'signTransaction' in adapter ? signTransaction ( adapter , connected , this . _error ) ( transaction ) : undefined ; } signAllTransactions ( transactions ) { const { adapter , connected } = this . get ( ) ; return adapter && 'signAllTransactions' in adapter ? signAllTransactions ( adapter , connected , this . _error ) ( transactions ) : undefined ; } signMessage ( message ) { const { adapter , connected } = this . get ( ) ; return adapter && 'signMessage' in adapter ? signMessage ( adapter , connected , this . _error ) ( message ) : undefined ; } }