import { assertNever , flattenAirGapTxAddresses , IACContext , KeyPairService , ProtocolService , sumAirGapTxValues , TransactionService } from '@airgap/angular-core' ; import { AirGapWallet , IACMessageDefinitionObjectV3 , IACMessageType , IAirGapTransaction , ICoinProtocol , MainProtocolSymbols , MessageSignRequest , ProtocolSymbols , SignedTransaction , TezosCryptoClient , TezosSaplingProtocol , UnsignedTransaction } from '@airgap/coinlib-core' ; import { Injectable } from '@angular/core' ; import { Actions , createEffect , ofType } from '@ngrx/effects' ; import { Action , Store } from '@ngrx/store' ; import { TranslateService } from '@ngx-translate/core' ; import * as bip39 from 'bip39' ; import { concat , from , of } from 'rxjs' ; import { concatMap , first , switchMap , tap , withLatestFrom } from 'rxjs/operators' ; import { MnemonicSecret } from '../../models/secret' ; import { SignTransactionInfo } from '../../models/sign-transaction-info' ; import { InteractionOperationType , InteractionService } from '../../services/interaction/interaction.service' ; import { NavigationService } from '../../services/navigation/navigation.service' ; import { SecretsService } from '../../services/secrets/secrets.service' ; import * as actions from './deserialized-detail.actions' ; import * as fromDeserializedDetail from './deserialized-detail.reducer' ; import { DeserializedSignedMessage , DeserializedSignedTransaction , DeserializedUnsignedMessage , DeserializedUnsignedTransaction , Mode , Payload , Task } from './deserialized.detail.types' ; @ Injectable ( ) export class DeserializedDetailEffects { public navigationData$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( actions . viewInitialization ) , switchMap ( ( ) => concat ( of ( actions . navigationDataLoading ( ) ) , from ( this . loadNavigationData ( ) ) . pipe ( first ( ) ) ) ) ) ) ; public approved$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( actions . approved , actions . bip39PassphraseProvided , actions . signingProtocolProvided ) , concatMap ( ( action ) => from ( [ action ] ) . pipe ( withLatestFrom ( this . store . select ( fromDeserializedDetail . selectFinalPayload ) ) ) ) , switchMap ( ( [ action , payload ] ) => { const userInput = { bip39Passphrase : 'passphrase' in action ? action . passphrase : undefined , protocol : 'protocol' in action ? action . protocol : undefined } ; return concat ( of ( actions . runningBlockingTask ( { task : this . identifyPayloadTask ( payload ) , userInput } ) ) ) ; } ) ) ) ; public continueApproved$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( actions . continueApproved ) , concatMap ( ( action ) => from ( [ action ] ) . pipe ( withLatestFrom ( this . store . select ( fromDeserializedDetail . selectFinalPayload ) ) ) ) , switchMap ( ( [ action , payload ] ) => from ( this . handlePayload ( payload , action . userInput ) ) . pipe ( first ( ) ) ) ) ) ; public navigateWithSignedTransactions$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( actions . transactionsSigned ) , tap ( ( action ) => this . navigateWithSignedTransactions ( action . transactions ) ) ) , { dispatch : false } ) ; public navigateWithSignedMessages$ = createEffect ( ( ) => this . actions$ . pipe ( ofType ( actions . messagesSigned ) , tap ( ( action ) => this . navigateWithSignedMessages ( action . messages ) ) ) , { dispatch : false } ) ; constructor ( private readonly actions$ , private readonly store , private readonly navigationService , private readonly translateService , private readonly protocolService , private readonly secretsService , private readonly keyPairService , private readonly transactionService , private readonly interactionService ) { } private async loadNavigationData ( ) { const state = this . navigationService . getState ( ) ; const [ , messageType ] = this . getPageTypes ( state . type ) ; const mode = this . getMode ( state . type ) ; if ( messageType !== undefined && mode !== undefined && state . transactionInfos !== undefined ) { const title = this . translateService . instant ( `deserialized-detail.${ messageType }.title` ) ; const button = this . translateService . instant ( `deserialized-detail.${ messageType }.button_label` ) ; const raw = this . signTransactionInfoToRaw ( state . transactionInfos ) ; try { const [ transactions , messages ] = await Promise . all ( [ this . signTransactionInfoToUnsignedTransactions ( state . transactionInfos , state . iacContext ) , this . signTransactionInfoToUnsignedMessages ( state . transactionInfos , state . iacContext ) ] ) ; return actions . navigationDataLoaded ( { mode , title , button , transactions , messages , raw } ) ; } catch ( error ) { console . warn ( error ) ; return actions . navigationDataLoadingError ( { mode , title , button , raw } ) ; } } else { return actions . invalidData ( ) ; } } private getPageTypes ( iacMessageType ) { let actionType ; if ( iacMessageType === IACMessageType . TransactionSignRequest || iacMessageType === IACMessageType . MessageSignRequest ) { actionType = 'request' ; } else if ( iacMessageType === IACMessageType . TransactionSignResponse || iacMessageType === IACMessageType . MessageSignResponse ) { actionType = 'response' ; } let messageType ; if ( iacMessageType === IACMessageType . TransactionSignRequest || iacMessageType === IACMessageType . TransactionSignResponse ) { messageType = 'transaction' ; } else if ( iacMessageType === IACMessageType . MessageSignRequest || iacMessageType === IACMessageType . MessageSignResponse ) { messageType = 'message' ; } return [ actionType , messageType ] ; } private getMode ( iacMessageType ) { switch ( iacMessageType ) { case IACMessageType . TransactionSignRequest : return Mode . SIGN_TRANSACTION ; case IACMessageType . MessageSignRequest : return Mode . SIGN_MESSAGE ; default : return undefined ; } } private signTransactionInfoToRaw ( transactionInfo ) { const transactions = transactionInfo . map ( ( info ) : unknown => { switch ( info . signTransactionRequest . type ) { case IACMessageType . TransactionSignRequest : return ( info . signTransactionRequest . payload as UnsignedTransaction ) . transaction ; case IACMessageType . TransactionSignResponse : return ( info . signTransactionRequest . payload as SignedTransaction ) . transaction ; default : return info . signTransactionRequest . payload ; } } ) ; const toStringify = transactions . length > 1 ? transactions : transactions [ 0 ] ; return JSON . stringify ( toStringify ) ; } private async signTransactionInfoToUnsignedTransactions ( transactionInfo , iacContext ? ) { return Promise . all ( transactionInfo . map ( ( info ) : [ AirGapWallet , IACMessageDefinitionObjectV3 ] => [ info . wallet , info . signTransactionRequest ] ) . filter ( ( [ _ , request ] ) : boolean => request . type === IACMessageType . TransactionSignRequest ) . map ( async ( [ wallet , request ] ) : Promise < DeserializedUnsignedTransaction > => { let details ; if ( await this . checkIfSaplingTransaction ( request . payload as UnsignedTransaction , request . protocol ) ) { details = await this . transactionService . getDetailsFromIACMessages ( [ request ] , { overrideProtocol : await this . getSaplingProtocol ( ) , data : { knownViewingKeys : this . secretsService . getKnownViewingKeys ( ) } } ) ; } else { details = await this . transactionService . getDetailsFromIACMessages ( [ request ] ) ; } return { type : 'unsigned' , id : request . id , details , data : request . payload as UnsignedTransaction , iacContext , wallet , originalProtocolIdentifier : request . protocol !== wallet . protocol . symbol ? request . protocol : undefined } ; } ) ) ; } private async signTransactionInfoToUnsignedMessages ( transactionInfo , iacContext ? ) { return Promise . all ( transactionInfo . map ( ( info ) : [ AirGapWallet , IACMessageDefinitionObjectV3 ] => [ info . wallet , info . signTransactionRequest ] ) . filter ( ( [ _ , request ] ) : boolean => request . type === IACMessageType . MessageSignRequest ) . map ( async ( [ wallet , request ] ) : Promise < DeserializedUnsignedMessage > => { const data = request . payload as MessageSignRequest ; let blake2bHash ; if ( request . protocol === MainProtocolSymbols . XTZ ) { const cryptoClient = new TezosCryptoClient ( ) ; blake2bHash = await cryptoClient . blake2bLedgerHash ( data . message ) ; } return { type : 'unsigned' , id : request . id , protocol : request . protocol , data , iacContext , blake2bHash , wallet , originalProtocolIdentifier : request . protocol !== wallet . protocol . symbol ? request . protocol : undefined } ; } ) ) ; } private identifyPayloadTask ( payload ) { switch ( payload . mode ) { case Mode . SIGN_TRANSACTION : return 'signTransaction' ; case Mode . SIGN_MESSAGE : return 'signMessage' ; default : return 'generic' ; } } private async handlePayload ( payload , userInput ) { switch ( payload . mode ) { case Mode . SIGN_TRANSACTION : return this . signTransactions ( payload . data , userInput . bip39Passphrase ) ; case Mode . SIGN_MESSAGE : return this . signMessages ( payload . data , userInput . bip39Passphrase , userInput . protocol ) ; default : assertNever ( 'handlePayload' , payload ) ; } } private async signTransactions ( unsignedTransactions , bip39Passphrase = '' ) { try { const signedTransactions = await Promise . all ( unsignedTransactions . map ( async ( transaction ) : Promise < DeserializedSignedTransaction > => { const signed = await this . signTransaction ( transaction . wallet , transaction . data , transaction . iacContext , bip39Passphrase ) ; return { type : 'signed' , id : transaction . id , details : transaction . details , data : { accountIdentifier : transaction . wallet . publicKey . substr ( - 6 ) , transaction : signed , callbackURL : transaction . data . callbackURL } , iacContext : transaction . iacContext , wallet : transaction . wallet , originalProtocolIdentifier : transaction . originalProtocolIdentifier } ; } ) ) ; return actions . transactionsSigned ( { transactions : signedTransactions } ) ; } catch ( error ) { console . warn ( error ) ; if ( error . message ?. toLowerCase ( ) . startsWith ( 'secret not found' ) ) { return actions . secretNotFound ( ) ; } else if ( error . message ?. toLowerCase ( ) . startsWith ( 'invalid bip-39 passphrase' ) ) { return actions . invalidBip39Passphrase ( ) ; } return actions . unknownError ( { message : typeof error === 'string' ? error : error . message } ) ; } } private async signTransaction ( wallet , transaction , iacContext , bip39Passphrase ) { const secret = this . secretsService . findByPublicKey ( wallet . publicKey ) ; if ( secret === undefined ) { throw new Error ( 'Secret not found' ) ; } const entropy = await this . secretsService . retrieveEntropyForSecret ( secret ) ; const mnemonic = bip39 . entropyToMnemonic ( entropy ) ; return this . keyPairService . signWithProtocol ( wallet . protocol , transaction , mnemonic , bip39Passphrase , wallet . isExtendedPublicKey , wallet . derivationPath , await this . getChildDerivationPath ( wallet . derivationPath , iacContext ?. derivationPath ) ) ; } private async signMessages ( unsignedMessages , bip39Passphrase = '' , protocolIdentifier ? ) { try { const signedMessages = await Promise . all ( unsignedMessages . map ( async ( message ) : Promise < DeserializedSignedMessage > => { const signature = await this . signMessage ( message . data , message . iacContext , bip39Passphrase , message . wallet , protocolIdentifier ) ; return { type : 'signed' , id : message . id , protocol : message . protocol ?? protocolIdentifier , data : { message : message . data . message , publicKey : message . data . publicKey , signature , callbackURL : message . data . callbackURL } , iacContext : message . iacContext , wallet : message . wallet , originalProtocolIdentifier : message . originalProtocolIdentifier } ; } ) ) ; return actions . messagesSigned ( { messages : signedMessages } ) ; } catch ( error ) { console . warn ( error ) ; if ( error . message ?. toLowerCase ( ) . startsWith ( 'secret not found' ) ) { return actions . secretNotFound ( ) ; } else if ( error . message ?. toLowerCase ( ) . startsWith ( 'protocol not found' ) ) { return actions . protocolNotFound ( ) ; } else if ( error . message ?. toLowerCase ( ) . startsWith ( 'invalid bip-39 passphrase' ) ) { return actions . invalidBip39Passphrase ( ) ; } return actions . unknownError ( { message : typeof error === 'string' ? error : error . message } ) ; } } private async signMessage ( message , iacContext , bip39Passphrase , wallet ? , protocolIdentifier ? ) { const secret = wallet !== undefined ? this . secretsService . findByPublicKey ( wallet . publicKey ) ?? this . secretsService . getActiveSecret ( ) : this . secretsService . getActiveSecret ( ) ; if ( secret === undefined ) { throw new Error ( 'Secret not found' ) ; } const entropy = await this . secretsService . retrieveEntropyForSecret ( secret ) ; const mnemonic = bip39 . entropyToMnemonic ( entropy ) ; if ( wallet !== undefined ) { return this . keyPairService . signWithProtocol ( wallet . protocol , message , mnemonic , bip39Passphrase , wallet . isExtendedPublicKey , wallet . derivationPath , await this . getChildDerivationPath ( wallet . derivationPath , iacContext ?. derivationPath ) ) ; } else { let protocol ; try { protocol = protocolIdentifier !== undefined ? await this . protocolService . getProtocol ( protocolIdentifier , undefined , false ) : undefined ; } catch ( error ) { console . warn ( error ) ; protocol = undefined ; } if ( protocol === undefined ) { throw new Error ( 'Protocol not found' ) ; } return this . keyPairService . signWithProtocol ( protocol , message , mnemonic , bip39Passphrase , false , protocol . standardDerivationPath , await this . getChildDerivationPath ( protocol . standardDerivationPath , iacContext ?. derivationPath ) ) ; } } private async navigateWithSignedTransactions ( transactions ) { const messages = await this . generateTransactionIACMessages ( transactions ) ; this . interactionService . startInteraction ( { operationType : InteractionOperationType . TRANSACTION_BROADCAST , iacMessage : messages , wallets : transactions . map ( ( transaction ) : AirGapWallet => transaction . wallet ) , signedTxs : transactions . map ( ( transaction ) : string => transaction . data . transaction ) } ) ; } private async generateTransactionIACMessages ( transactions ) { const signResponses = transactions . map ( ( transaction ) : IACMessageDefinitionObjectV3 => ( { id : transaction . id , protocol : transaction . originalProtocolIdentifier ?? transaction . wallet . protocol . identifier , type : IACMessageType . TransactionSignResponse , payload : { accountIdentifier : transaction . data . accountIdentifier , transaction : transaction . data . transaction , from : flattenAirGapTxAddresses ( transaction . details , 'from' ) , to : flattenAirGapTxAddresses ( transaction . details , 'to' ) , amount : sumAirGapTxValues ( transaction . details , 'amount' ) , fee : sumAirGapTxValues ( transaction . details , 'fee' ) } } ) ) ; return signResponses ; } private async navigateWithSignedMessages ( messages ) { const iacMessages = await this . generateIACMessages ( messages ) ; this . interactionService . startInteraction ( { operationType : InteractionOperationType . MESSAGE_SIGN_REQUEST , iacMessage : iacMessages , messageSignResponse : messages [ 0 ] !== undefined ? { message : messages [ 0 ] . data . message , publicKey : messages [ 0 ] . data . publicKey , signature : messages [ 0 ] . data . signature } : undefined } ) ; } private async generateIACMessages ( messages ) { const signResponses = messages . map ( ( message ) : IACMessageDefinitionObjectV3 => ( { id : message . id , protocol : message . originalProtocolIdentifier ?? message . protocol , type : IACMessageType . MessageSignResponse , payload : { message : message . data . message , publicKey : message . data . publicKey , signature : message . data . signature } } ) ) ; return signResponses ; } private async checkIfSaplingTransaction ( transaction , protocolIdentifier ) { if ( protocolIdentifier === MainProtocolSymbols . XTZ ) { const tezosProtocol = await this . protocolService . getProtocol ( protocolIdentifier ) ; const saplingProtocol = await this . getSaplingProtocol ( ) ; const txDetails = await tezosProtocol . getTransactionDetails ( transaction ) ; const recipients = txDetails . map ( ( details ) => details . to ) . reduce ( ( flatten , next ) => flatten . concat ( next ) , [ ] ) ; return recipients . includes ( saplingProtocol . options . config . contractAddress ) ; } return protocolIdentifier === MainProtocolSymbols . XTZ_SHIELDED ; } private async getSaplingProtocol ( ) { return ( await this . protocolService . getProtocol ( MainProtocolSymbols . XTZ_SHIELDED ) ) as TezosSaplingProtocol ; } private async getChildDerivationPath ( walletDerivationPath , accountDerivationPath ? ) { if ( ! accountDerivationPath ) { return undefined ; } let normalizedDerivationPath = accountDerivationPath ; if ( ! normalizedDerivationPath . startsWith ( 'm' ) ) { normalizedDerivationPath = `m/${ normalizedDerivationPath }` ; } if ( ! normalizedDerivationPath . startsWith ( walletDerivationPath ) ) { throw new Error ( 'Derivation paths do not match!' ) ; } return normalizedDerivationPath . slice ( walletDerivationPath . length + 1 ) ; } }