import { assertUnreachable , deriveNetworkFromAddress , getNetworkDefinitionFromIdentifier , NetworkSummariesDocument , validatorAddressToOperatorAddress , wait , } from "@anthem/utils" ; import { LEDGER_ERRORS } from "constants/ledger-errors" ; import { getAccAddress } from "lib/terra-lib/key-utils" ; import { EpicSignature , ReduxActionTypes } from "modules/root" ; import { i18nSelector } from "modules/settings/selectors" ; import { combineEpics } from "redux-observable" ; import { from } from "rxjs" ; import { filter , ignoreElements , map , mergeMap , pluck , takeUntil , tap , } from "rxjs/operators" ; import Analytics from "tools/analytics-utils" ; import { capitalizeString , getQueryParamsFromUrl , isChartTabValidForNetwork , onChartTab , onPageWhichIncludesAddressParam , } from "tools/client-utils" ; import StorageModule from "tools/storage-utils" ; import { validateLedgerAppVersion , validateNetworkAddress , } from "tools/validation-utils" ; import { isActionOf } from "typesafe-actions" ; import Toast from "ui/Toast" ; import { Actions } from "../root-actions" ; import selectors , { addressSelector } from "./selectors" ; /** ===========================================================================
 * Epics
 * ============================================================================
 */ const setAddressEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . setAddress ) ) , mergeMap ( async ( { payload , meta } ) => { let setAddress = payload ; const { tString } = i18nSelector ( state$ . value ) ; const address = addressSelector ( state$ . value ) ; if ( payload . length > 52 ) { return Actions . searchTransactionByHash ( payload ) ; } if ( setAddress . includes ( "valoper" ) ) { setAddress = validatorAddressToOperatorAddress ( setAddress ) ; } const maybeErrorMessage = validateNetworkAddress ( setAddress , address , tString ) ; if ( maybeErrorMessage ) { if ( meta . showToastForError ) { Toast . warn ( maybeErrorMessage ) ; } return Actions . setAddressFailure ( maybeErrorMessage ) ; } StorageModule . setAddress ( setAddress ) ; StorageModule . updateRecentAddress ( setAddress ) ; Analytics . setAddress ( setAddress ) ; const network = deriveNetworkFromAddress ( setAddress ) ; return Actions . setAddressSuccess ( { network , address : setAddress , } ) ; } ) ) ; } ; /**
 * Epic to handle connecting a Ledger in response to various actions. Maps
 * to the connect action if the Ledger is not currently connected and
 * if the Ledger Dialog action has been dispatched.
 */ const ledgerDialogConnectionEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . openLedgerDialog ) ) , pluck ( "payload" ) , pluck ( "signinType" ) , filter ( x => x === "LEDGER" ) , filter ( ( ) => ! state$ . value . ledger . ledger . connected ) , map ( ( ) => Actions . connectLedger ( ) ) ) ; } ; const connectLedgerEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . connectLedger ) ) , mergeMap ( ( ) => { return from ( new Promise < ReduxActionTypes > ( async ( resolve ) => { try { const { cosmosLedgerUtil , celoLedgerUtil , oasisLedgerUtil } = deps ; const { signinNetworkName } = selectors . ledgerDialogSelector ( state$ . value ) ; if ( ! signinNetworkName ) { throw new Error ( "No signin network selected" ) ; } const networkDefinition = getNetworkDefinitionFromIdentifier ( signinNetworkName ) ; if ( ! networkDefinition . supportsLedger ) { Toast . warn ( `${ networkDefinition . name } Network is not supported on Ledger yet.` ) ; return resolve ( Actions . connectLedgerFailure ( ) ) ; } let celoAddressHasAccount = false ; let ledgerAddress ; let ledgerAppVersion = "0.0.0" ; switch ( signinNetworkName ) { case "COSMOS" : { await cosmosLedgerUtil . connectDevice ( ) ; ledgerAddress = await cosmosLedgerUtil . getCosmosAddress ( ) ; ledgerAppVersion = await cosmosLedgerUtil . getCosmosAppVersion ( ) ; break ; } case "KAVA" : case "TERRA" : { await cosmosLedgerUtil . connectDevice ( ) ; const pk = await cosmosLedgerUtil . getPubKey ( ) ; if ( typeof pk === "string" ) { ledgerAddress = getAccAddress ( Buffer . from ( pk ) , signinNetworkName ) ; } else { ledgerAddress = getAccAddress ( pk , signinNetworkName ) ; } ledgerAppVersion = await cosmosLedgerUtil . getCosmosAppVersion ( ) ; break ; } case "CELO" : { await celoLedgerUtil . connect ( ) ; ledgerAddress = await celoLedgerUtil . getAddress ( ) ; ledgerAppVersion = await celoLedgerUtil . getCeloAppVersion ( ) ; celoAddressHasAccount = await deps . celoLedgerUtil . isAccount ( ledgerAddress ) ; break ; } case "OASIS" : { await oasisLedgerUtil . connect ( ) ; ledgerAddress = await oasisLedgerUtil . getAddress ( ) ; ledgerAppVersion = await oasisLedgerUtil . getVersion ( ) ; break ; } case "POLKADOT" : return Actions . empty ( "Polkadot Ledger is not supported." ) ; default : { return assertUnreachable ( signinNetworkName ) ; } } const network = deriveNetworkFromAddress ( ledgerAddress ) ; const versionValid = validateLedgerAppVersion ( ledgerAppVersion , network . ledgerAppVersion ) ; if ( ! versionValid ) { Toast . danger ( `Invalid ${ network . ledgerAppName } version! Please upgrade your ${ network . ledgerAppName } Ledger application!` ) ; return resolve ( Actions . connectLedgerFailure ( ) ) ; } Toast . success ( `Ledger connected to ${ capitalizeString ( signinNetworkName ) } Network!` ) ; return resolve ( Actions . connectLedgerSuccess ( { network , ledgerAppVersion , ledgerAddress , celoAddressHasAccount , } ) ) ; } catch ( error ) { console . log ( error ) ; let retryDelay = 2500 ; const { message } = error ; if ( message === LEDGER_ERRORS . BROWSER_NOT_SUPPORTED ) { Toast . warn ( "This browser is not supported." ) ; } else if ( message === LEDGER_ERRORS . COSMOS_LEDGER_SCREENSAVER_ERROR ) { const { tString } = i18nSelector ( state$ . value ) ; Toast . warn ( tString ( "The screensaver mode is currently active on the Ledger Device" ) ) ; retryDelay = 6500 ; } await wait ( retryDelay ) ; return resolve ( Actions . connectLedger ( ) ) ; } } ) ) . pipe ( takeUntil ( action$ . ofType ( Actions . closeLedgerDialog ( ) . type ) ) ) ; } ) ) ; } ; /**
 * Redirect to the dashboard if logging in on the /networks page.
 */ const redirectFromNetworkPageEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( [ Actions . connectLedgerSuccess , Actions . setAddressSuccess ] ) ) , filter ( ( ) => { return window . location . pathname . includes ( "/networks" ) ; } ) , tap ( ( ) => { deps . router . push ( "/" ) ; } ) ) ; } ; /**
 * Handling creating an account for a Celo Ledger address. This is a one
 * time action.
 */ const celoCreateAccountEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . setCeloAccountStage ) ) , pluck ( "payload" ) , filter ( stage => stage === "SIGN" ) , mergeMap ( async ( ) => { try { const { address } = state$ . value . ledger . ledger ; await deps . celoLedgerUtil . createAccount ( address ) ; return Actions . setCeloAccountStage ( "CONFIRMED" ) ; } catch ( err ) { Toast . warn ( "Create account failed..." ) ; return Actions . setCeloAccountStage ( "SETUP" ) ; } } ) ) ; } ; const logoutEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . confirmLogout ) ) , tap ( ( ) => { StorageModule . logout ( ) ; const { client } = deps ; client . cache . reset ( ) ; Analytics . logout ( ) ; deps . router . push ( "/networks" ) ; const fiat = state$ . value . settings . fiatCurrency . symbol ; client . query ( { query : NetworkSummariesDocument , variables : { fiat } , } ) ; const { tString } = i18nSelector ( state$ . value ) ; Toast . success ( tString ( "Logout Success." ) ) ; } ) , map ( Actions . logoutSuccess ) ) ; } ; const saveAddressEpic = action$ => { return action$ . pipe ( filter ( isActionOf ( [ Actions . connectLedgerSuccess , Actions . setAddressSuccess ] ) ) , pluck ( "payload" ) , tap ( payload => { let address = "" ; if ( "ledgerAddress" in payload ) { address = payload . ledgerAddress ; } else { address = payload . address ; } StorageModule . setAddress ( address ) ; StorageModule . updateRecentAddress ( address ) ; } ) , ignoreElements ( ) ) ; } ; /**
 * When the address updates sync it to the url if it's out of sync.
 */ const syncAddressToUrlEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( [ Actions . setAddressSuccess , Actions . connectLedgerSuccess ] ) ) , filter ( ( ) => { const { location } = deps . router ; return onPageWhichIncludesAddressParam ( location . pathname ) ; } ) , pluck ( "payload" ) , tap ( ( { network } ) => { const { address } = state$ . value . ledger . ledger ; const { transactionsPage } = state$ . value . transaction ; const { location } = deps . router ; const { pathname } = location ; const tab = pathname . split ( "/" ) [ 2 ] ; const onChartView = onChartTab ( tab ) ; const onValidChartTab = isChartTabValidForNetwork ( tab , network ) ; const search = transactionsPage > 1 ? `?address=${ address }&page=${ transactionsPage }` : `?address=${ address }` ; if ( search !== location . search ) { deps . router . replace ( { search } ) ; } const name = network . name . toLowerCase ( ) ; const onDifferentNetwork = ! pathname . includes ( name ) ; if ( ( ! onValidChartTab && onChartView ) || onDifferentNetwork ) { deps . router . replace ( { search , pathname : `/${ name }/total` , } ) ; } } ) , ignoreElements ( ) ) ; } ; /**
 * Set the address from routing events, if they routed address is
 * different from the stored address.
 */ const setAddressOnNavigationEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . onRouteChange ) ) , pluck ( "payload" ) , pluck ( "search" ) , map ( search => { const { address } = getQueryParamsFromUrl ( search ) ; const ledger = state$ . value . ledger ; if ( deps . router . location . pathname . includes ( "landing" ) ) { return Actions . empty ( "No action taken to update location query params" ) ; } if ( typeof address === "string" && address !== ledger . ledger . address ) { return Actions . setAddress ( address , { showToastForError : false } ) ; } else { return Actions . empty ( "No action taken to update location query params" ) ; } } ) ) ; } ; /**
 * Sync the address to the url after navigation events.
 */ const syncAddressToUrlOnNavigationEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . onRouteChange ) ) , pluck ( "payload" ) , tap ( ( ) => { const { address } = state$ . value . ledger . ledger ; const { transactionsPage } = state$ . value . transaction ; const { locationState } = state$ . value . app . app ; const params = getQueryParamsFromUrl ( locationState . search ) ; const search = transactionsPage > 1 ? `?address=${ address }&page=${ transactionsPage }` : `?address=${ address }` ; if ( ! ! address && onPageWhichIncludesAddressParam ( locationState . pathname ) && ! params . address ) { deps . router . replace ( { search } ) ; } } ) , ignoreElements ( ) ) ; } ; /**
 * Sync the address to the url when the app initializes.
 */ const syncAddressToUrlOnInitializationEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . initializeAppSuccess ) ) , pluck ( "payload" ) , pluck ( "address" ) , tap ( ( ) => { const { location } = deps . router ; const { address } = state$ . value . ledger . ledger ; const { transactionsPage } = state$ . value . transaction ; const search = transactionsPage > 1 ? `?address=${ address }&page=${ transactionsPage }` : `?address=${ address }` ; if ( onPageWhichIncludesAddressParam ( location . pathname ) && ! location . search . includes ( address ) ) { deps . router . replace ( { search } ) ; } } ) , ignoreElements ( ) ) ; } ; const searchTransactionNavigationEpic = ( action$ , state$ , deps ) => { return action$ . pipe ( filter ( isActionOf ( Actions . searchTransactionByHash ) ) , pluck ( "payload" ) , tap ( hash => { const { network } = state$ . value . ledger . ledger ; const pathname = `/${ network . name . toLowerCase ( ) }/txs/${ hash . toLowerCase ( ) }` ; deps . router . push ( { pathname } ) ; } ) , ignoreElements ( ) ) ; } ; const clearAllRecentAddressesEpic = action$ => { return action$ . pipe ( filter ( isActionOf ( Actions . clearAllRecentAddresses ) ) , tap ( StorageModule . clearRecentAddresses ) , ignoreElements ( ) ) ; } ; /** ===========================================================================
 * Export
 * ============================================================================
 */ export default combineEpics ( setAddressEpic , ledgerDialogConnectionEpic , connectLedgerEpic , logoutEpic , saveAddressEpic , syncAddressToUrlEpic , celoCreateAccountEpic , redirectFromNetworkPageEpic , syncAddressToUrlOnNavigationEpic , syncAddressToUrlOnInitializationEpic , setAddressOnNavigationEpic , searchTransactionNavigationEpic , clearAllRecentAddressesEpic ) ;