From b3882556aee9c612d414293a34a79140ef0f7f0d Mon Sep 17 00:00:00 2001 From: veado <61792675+veado@users.noreply.github.com> Date: Thu, 18 Aug 2022 20:09:57 +0200 Subject: [PATCH] Sync keystore/ledger states (#2361) - [x] Remove related ledger addresses by removing a wallet - [x] `keystore` service: Rename `kestore$` -> `keystoreState$` / `keystoreWallets` -> `keystoreWalletsPersistent$` etc. - [x] Subscribe to `keystoreWalletsPersistent$` to update internal state in memory - [x] Fix: Wallet change from WalletSettings failed --- src/renderer/components/header/Header.tsx | 2 +- .../components/settings/WalletSettings.tsx | 20 ++- src/renderer/hooks/useKeystoreState.ts | 10 +- src/renderer/hooks/useKeystoreWallets.ts | 8 +- src/renderer/services/binance/common.ts | 2 +- src/renderer/services/bitcoin/common.ts | 2 +- src/renderer/services/bitcoincash/common.ts | 2 +- src/renderer/services/cosmos/common.ts | 2 +- src/renderer/services/doge/common.ts | 2 +- src/renderer/services/ethereum/common.ts | 2 +- src/renderer/services/litecoin/common.ts | 2 +- src/renderer/services/thorchain/common.ts | 2 +- src/renderer/services/wallet/index.ts | 5 +- src/renderer/services/wallet/keystore.ts | 119 ++++++++++-------- src/renderer/services/wallet/ledger.ts | 51 +++++++- src/renderer/services/wallet/types.ts | 9 +- src/renderer/views/app/AppView.tsx | 8 +- src/renderer/views/deposit/DepositView.tsx | 2 +- src/renderer/views/swap/SwapView.tsx | 4 +- src/renderer/views/wallet/WalletAuth.tsx | 2 +- .../views/wallet/WalletSettingsAuth.tsx | 13 +- src/shared/api/types.ts | 2 +- src/shared/mock/api.ts | 2 +- 23 files changed, 177 insertions(+), 96 deletions(-) diff --git a/src/renderer/components/header/Header.tsx b/src/renderer/components/header/Header.tsx index 5a87c9145..612c2a656 100644 --- a/src/renderer/components/header/Header.tsx +++ b/src/renderer/components/header/Header.tsx @@ -19,7 +19,7 @@ export const Header: React.FC = (): JSX.Element => { const { keystoreService } = useWalletContext() const { mimir$ } = useThorchainContext() const { lock } = keystoreService - const keystore = useObservableState(keystoreService.keystore$, O.none) + const keystore = useObservableState(keystoreService.keystoreState$, O.none) const mimir = useObservableState(mimir$, RD.initial) const { service: midgardService } = useMidgardContext() const { diff --git a/src/renderer/components/settings/WalletSettings.tsx b/src/renderer/components/settings/WalletSettings.tsx index 2ccad49b4..3dacea90f 100644 --- a/src/renderer/components/settings/WalletSettings.tsx +++ b/src/renderer/components/settings/WalletSettings.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { SearchOutlined } from '@ant-design/icons' import * as RD from '@devexperts/remote-data-ts' @@ -453,10 +453,8 @@ export const WalletSettings: React.FC = (props): JSX.Element => { const changeWalletHandler = useCallback( (id: KeystoreId) => { subscribeChangeWalletState(changeKeystoreWallet$(id)) - // Jump to `UnlockView` to avoid staying at wallet settings - navigate(walletRoutes.locked.path()) }, - [changeKeystoreWallet$, navigate, subscribeChangeWalletState] + [changeKeystoreWallet$, subscribeChangeWalletState] ) const renderChangeWalletError = useMemo( @@ -478,6 +476,13 @@ export const WalletSettings: React.FC = (props): JSX.Element => { [changeWalletState, intl] ) + useEffect(() => { + if (RD.isSuccess(changeWalletState)) { + // Jump to `UnlockView` to avoid staying at wallet settings + navigate(walletRoutes.locked.path()) + } + }, [changeWalletState, navigate]) + const { state: renameWalletState, subscribe: subscribeRenameWalletState } = useSubscriptionState(RD.initial) @@ -612,7 +617,12 @@ export const WalletSettings: React.FC = (props): JSX.Element => {

{intl.formatMessage({ id: 'wallet.change.title' })}

- + {renderChangeWalletError} diff --git a/src/renderer/hooks/useKeystoreState.ts b/src/renderer/hooks/useKeystoreState.ts index 3dd903f7b..f46778c68 100644 --- a/src/renderer/hooks/useKeystoreState.ts +++ b/src/renderer/hooks/useKeystoreState.ts @@ -27,7 +27,7 @@ export const useKeystoreState = (): { } => { const { keystoreService: { - keystore$, + keystoreState$, unlock, lock, removeKeystoreWallet: remove, @@ -36,11 +36,11 @@ export const useKeystoreState = (): { } } = useWalletContext() - const state = useObservableState(keystore$, INITIAL_KEYSTORE_STATE) + const state = useObservableState(keystoreState$, INITIAL_KEYSTORE_STATE) - const [phrase] = useObservableState(() => FP.pipe(keystore$, RxOp.map(FP.flow(getPhrase))), O.none) - const [walletName] = useObservableState(() => FP.pipe(keystore$, RxOp.map(FP.flow(getWalletName))), O.none) - const [locked] = useObservableState(() => FP.pipe(keystore$, RxOp.map(FP.flow(isLocked))), false) + const [phrase] = useObservableState(() => FP.pipe(keystoreState$, RxOp.map(FP.flow(getPhrase))), O.none) + const [walletName] = useObservableState(() => FP.pipe(keystoreState$, RxOp.map(FP.flow(getWalletName))), O.none) + const [locked] = useObservableState(() => FP.pipe(keystoreState$, RxOp.map(FP.flow(isLocked))), false) return { state, phrase, walletName, unlock, lock, locked, remove, change$, rename$ } } diff --git a/src/renderer/hooks/useKeystoreWallets.ts b/src/renderer/hooks/useKeystoreWallets.ts index d72747288..cae01c6c5 100644 --- a/src/renderer/hooks/useKeystoreWallets.ts +++ b/src/renderer/hooks/useKeystoreWallets.ts @@ -6,16 +6,16 @@ import { useWalletContext } from '../contexts/WalletContext' import { KeystoreWalletsRD, KeystoreWalletsUI } from '../services/wallet/types' export const useKeystoreWallets = (): { - wallets: KeystoreWalletsRD + walletsPersistentRD: KeystoreWalletsRD reload: FP.Lazy walletsUI: KeystoreWalletsUI } => { const { - keystoreService: { reloadKeystoreWallets: reload, keystoreWallets$, keystoreWalletsUI$ } + keystoreService: { reloadPersistentKeystoreWallets: reload, keystoreWalletsPersistent$, keystoreWalletsUI$ } } = useWalletContext() - const wallets = useObservableState(keystoreWallets$, RD.initial) + const walletsPersistentRD = useObservableState(keystoreWalletsPersistent$, RD.initial) const walletsUI = useObservableState(keystoreWalletsUI$, []) - return { wallets: wallets, walletsUI, reload } + return { walletsPersistentRD, walletsUI, reload } } diff --git a/src/renderer/services/binance/common.ts b/src/renderer/services/binance/common.ts index 1791864f8..c85040cec 100644 --- a/src/renderer/services/binance/common.ts +++ b/src/renderer/services/binance/common.ts @@ -22,7 +22,7 @@ import { ClientState, ClientState$, Client$ } from './types' * A `BinanceClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/bitcoin/common.ts b/src/renderer/services/bitcoin/common.ts index 2628acb0b..f619d4a54 100644 --- a/src/renderer/services/bitcoin/common.ts +++ b/src/renderer/services/bitcoin/common.ts @@ -24,7 +24,7 @@ import { ClientState, ClientState$ } from './types' * A `BitcoinClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/bitcoincash/common.ts b/src/renderer/services/bitcoincash/common.ts index 0a53f1bfc..82ba265dd 100644 --- a/src/renderer/services/bitcoincash/common.ts +++ b/src/renderer/services/bitcoincash/common.ts @@ -24,7 +24,7 @@ import { ClientState, ClientState$ } from './types' * A `BitcoinCashClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/cosmos/common.ts b/src/renderer/services/cosmos/common.ts index e55d17ed3..43d5fa8f1 100644 --- a/src/renderer/services/cosmos/common.ts +++ b/src/renderer/services/cosmos/common.ts @@ -22,7 +22,7 @@ import type { Client$, ClientState, ClientState$ } from './types' * A `CosmosClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$, Rx.of(getClientUrls())]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$, Rx.of(getClientUrls())]), RxOp.switchMap( ([keystore, network, clientUrls]): ClientState$ => FP.pipe( diff --git a/src/renderer/services/doge/common.ts b/src/renderer/services/doge/common.ts index abaa0faf3..3a4bfd589 100644 --- a/src/renderer/services/doge/common.ts +++ b/src/renderer/services/doge/common.ts @@ -24,7 +24,7 @@ import { ClientState, ClientState$ } from './types' * A `DogeClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/ethereum/common.ts b/src/renderer/services/ethereum/common.ts index 8f22e9b11..48292ddae 100644 --- a/src/renderer/services/ethereum/common.ts +++ b/src/renderer/services/ethereum/common.ts @@ -28,7 +28,7 @@ import { Client$, ClientState, ClientState$ } from './types' * A `EthereumClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/litecoin/common.ts b/src/renderer/services/litecoin/common.ts index a225897b4..236b07c2d 100644 --- a/src/renderer/services/litecoin/common.ts +++ b/src/renderer/services/litecoin/common.ts @@ -23,7 +23,7 @@ import { Client$, ClientState$, ClientState } from './types' * A `LitecoinClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$]), RxOp.switchMap( ([keystore, network]): ClientState$ => Rx.of( diff --git a/src/renderer/services/thorchain/common.ts b/src/renderer/services/thorchain/common.ts index 547113c6e..7dc2e1d05 100644 --- a/src/renderer/services/thorchain/common.ts +++ b/src/renderer/services/thorchain/common.ts @@ -23,7 +23,7 @@ import { Client$, ClientState, ClientState$ } from './types' * A `ThorchainClient` will never be created as long as no phrase is available */ const clientState$: ClientState$ = FP.pipe( - Rx.combineLatest([keystoreService.keystore$, clientNetwork$, Rx.of(getClientUrl())]), + Rx.combineLatest([keystoreService.keystoreState$, clientNetwork$, Rx.of(getClientUrl())]), RxOp.switchMap( ([keystore, network, clientUrl]): ClientState$ => FP.pipe( diff --git a/src/renderer/services/wallet/index.ts b/src/renderer/services/wallet/index.ts index 41c981598..49b743d9e 100644 --- a/src/renderer/services/wallet/index.ts +++ b/src/renderer/services/wallet/index.ts @@ -7,11 +7,12 @@ import { getTxs$, loadTxs, explorerUrl$, resetTxsPage } from './transaction' const { askLedgerAddress$, getLedgerAddress$, verifyLedgerAddress, removeLedgerAddress, ledgerAddresses$ } = createLedgerService({ - keystore$: keystoreService.keystore$ + keystore$: keystoreService.keystoreState$, + wallets$: keystoreService.keystoreWalletsUI$ }) const { reloadBalances, reloadBalancesByChain, balancesState$, chainBalances$ } = createBalancesService({ - keystore$: keystoreService.keystore$, + keystore$: keystoreService.keystoreState$, network$, getLedgerAddress$ }) diff --git a/src/renderer/services/wallet/keystore.ts b/src/renderer/services/wallet/keystore.ts index 5e6a3ba42..6850d0b62 100644 --- a/src/renderer/services/wallet/keystore.ts +++ b/src/renderer/services/wallet/keystore.ts @@ -47,19 +47,15 @@ const { get$: importingKeystoreState$, set: setImportingKeystoreState } = observ * State of selected keystore wallet */ const { - get$: getKeystoreState$, - get: getKeystoreState, + get$: keystoreState$, + get: keystoreState, set: setKeystoreState } = observableState(INITIAL_KEYSTORE_STATE) /** * Internal state of keystore wallets - not shared to outside world */ -const { - get$: getKeystoreWallets$, - get: getKeystoreWallets, - set: setKeystoreWallets -} = observableState([]) +const { get$: keystoreWallets$, get: keystoreWallets, set: setKeystoreWallets } = observableState([]) /** * Adds a keystore and saves it to disk @@ -71,7 +67,7 @@ const addKeystoreWallet = async ({ phrase, name, id, password }: AddKeystorePara // remove selected state from current wallets const wallets = FP.pipe( - getKeystoreWallets(), + keystoreWallets(), A.map((wallet) => ({ ...wallet, selected: false })) ) // Add new wallet to wallet list + mark it `selected` @@ -87,7 +83,7 @@ const addKeystoreWallet = async ({ phrase, name, id, password }: AddKeystorePara const encodedWallets = ipcKeystoreWalletsIO.encode(updatedWallets) // Save wallets to disk - await window.apiKeystore.saveKeystoreWallets(encodedWallets) + const _ = await window.apiKeystore.saveKeystoreWallets(encodedWallets) // Update states setKeystoreWallets(updatedWallets) setKeystoreState(O.some({ id, phrase, name })) @@ -100,7 +96,7 @@ const addKeystoreWallet = async ({ phrase, name, id, password }: AddKeystorePara } export const removeKeystoreWallet = async () => { - const state = getKeystoreState() + const state = keystoreState() const keystoreId = FP.pipe(state, getKeystoreId, O.toNullable) if (!keystoreId) { @@ -108,12 +104,12 @@ export const removeKeystoreWallet = async () => { } // Remove it from `wallets` const wallets = FP.pipe( - getKeystoreWallets(), + keystoreWallets(), A.filter(({ id }) => id !== keystoreId) ) const encodedWallets = ipcKeystoreWalletsIO.encode(wallets) // Save updated `wallets` to disk - await window.apiKeystore.saveKeystoreWallets(encodedWallets) + const _ = await window.apiKeystore.saveKeystoreWallets(encodedWallets) // Update states setKeystoreWallets(wallets) // Set previous to current wallets (if available) @@ -125,7 +121,7 @@ export const removeKeystoreWallet = async () => { } const changeKeystoreWallet: ChangeKeystoreWalletHandler = (keystoreId: KeystoreId) => { - const wallets = getKeystoreWallets() + const wallets = keystoreWallets() // Get selected wallet const selectedWallet = FP.pipe( wallets, @@ -143,20 +139,27 @@ const changeKeystoreWallet: ChangeKeystoreWalletHandler = (keystoreId: KeystoreI ) return FP.pipe( - updatedWallets, // encode wallets first - ipcKeystoreWalletsIO.encode, + Rx.of(ipcKeystoreWalletsIO.encode(updatedWallets)), // Save updated `wallets` to disk - (wallets) => Rx.from(window.apiKeystore.saveKeystoreWallets(wallets)), + RxOp.switchMap((wallets) => Rx.from(window.apiKeystore.saveKeystoreWallets(wallets))), + RxOp.map((eWallets) => + FP.pipe( + eWallets, + E.fold( + (error) => RD.failure(Error(`Could not save wallets on disc ${error?.message ?? error.toString()}`)), + (_) => { + // Update states + setKeystoreWallets(updatedWallets) + // set selected wallet as locked wallet + setKeystoreState(O.some({ id, name })) + + return RD.success(true) + } + ) + ) + ), RxOp.catchError((err) => Rx.of(RD.failure(err))), - RxOp.map(() => RD.success(true)), - liveData.map((_) => { - // Update states - setKeystoreWallets(updatedWallets) - // set selected wallet as locked wallet - setKeystoreState(O.some({ id, name })) - return true - }), RxOp.startWith(RD.pending) ) } @@ -164,7 +167,7 @@ const changeKeystoreWallet: ChangeKeystoreWalletHandler = (keystoreId: KeystoreI const renameKeystoreWallet: RenameKeystoreWalletHandler = (id, name) => { // get keystore state - needs to be UNLOCKED const updatedKeystoreState = FP.pipe( - getKeystoreState(), + keystoreState(), O.chain(FP.flow(O.fromPredicate(isKeystoreUnlocked))), // rename in state O.map((state) => ({ ...state, name })), @@ -176,7 +179,7 @@ const renameKeystoreWallet: RenameKeystoreWalletHandler = (id, name) => { // update selected wallet in list of wallets const updatedWallets = FP.pipe( - getKeystoreWallets(), + keystoreWallets(), A.map((wallet) => (id === wallet.id ? { ...wallet, name } : wallet)) ) return FP.pipe( @@ -215,12 +218,12 @@ const importKeystore = async ({ keystore, password, name, id }: ImportKeystorePa */ const exportKeystore = async () => { try { - const id = FP.pipe(getKeystoreState(), getKeystoreId, O.toNullable) + const id = FP.pipe(keystoreState(), getKeystoreId, O.toNullable) if (!id) { throw Error(`Can't export keystore - keystore id is missing in KeystoreState`) } - const wallets = getKeystoreWallets() + const wallets = keystoreWallets() const keystore = FP.pipe(wallets, getKeystore(id), O.toNullable) if (!keystore) { throw Error(`Can't export keystore - keystore is missing in wallet list`) @@ -248,7 +251,7 @@ const loadKeystore$ = (): LoadKeystoreLD => { } const lock = async () => { - const state = getKeystoreState() + const state = keystoreState() // make sure keystore is already imported if (!hasImportedKeystore(state)) { throw Error(`Can't lock - keystore seems not to be imported`) @@ -262,7 +265,7 @@ const lock = async () => { } const unlock = async (password: string) => { - const state = getKeystoreState() + const state = keystoreState() const lockedData = FP.pipe(state, getLockedData, O.toNullable) // make sure keystore is already imported if (!lockedData) { @@ -272,7 +275,7 @@ const unlock = async (password: string) => { const { id, name } = lockedData // get keystore from wallet list (not stored in `KeystoreState`) - const keystore = FP.pipe(getKeystoreWallets(), getKeystore(id), O.toNullable) + const keystore = FP.pipe(keystoreWallets(), getKeystore(id), O.toNullable) if (!keystore) { throw Error(`Can't unlock - keystore is missing in wallet list`) } @@ -285,11 +288,14 @@ const unlock = async (password: string) => { } } -// `TriggerStream` to reload data of `ThorchainLastblock` -const { stream$: reloadKeystoreWallets$, trigger: reloadKeystoreWallets } = triggerStream() +// `TriggerStream` to reload persistent `KeystoreWallets` +const { stream$: reloadPersistentKeystoreWallets$, trigger: reloadPersistentKeystoreWallets } = triggerStream() -const keystoreWallets$: KeystoreWalletsLD = FP.pipe( - reloadKeystoreWallets$, +/** + * Persistent `KeystoreWallets` stored on disc. + */ +const keystoreWalletsPersistent$: KeystoreWalletsLD = FP.pipe( + reloadPersistentKeystoreWallets$, RxOp.switchMap(() => Rx.from(window.apiKeystore.initKeystoreWallets())), RxOp.catchError((e) => Rx.of(E.left(e))), RxOp.switchMap( @@ -300,27 +306,35 @@ const keystoreWallets$: KeystoreWalletsLD = FP.pipe( ) ) ), - liveData.map((wallets) => { - const state = getInitialKeystoreData(wallets) - - setKeystoreState(state) - setKeystoreWallets(wallets) - - return wallets - }), RxOp.startWith(RD.pending), RxOp.shareReplay(1) ) +// Subscribe keystoreWalletsPersistent$ +// to update internal `KeystoreWallets` + `KeystoreState` stored in memory +// whenever data are loaded from disc, +keystoreWalletsPersistent$.subscribe((walletsRD) => + FP.pipe( + walletsRD, + RD.map((wallets) => { + const state = getInitialKeystoreData(wallets) + // update internal `KeystoreWallets` + `KeystoreState` stored in memory + setKeystoreState(state) + setKeystoreWallets(wallets) + return true + }) + ) +) + // Simplified `KeystoreWallets` (w/o loading state, w/o `keystore`) to display data at UIs const keystoreWalletsUI$: KeystoreWalletsUI$ = FP.pipe( - getKeystoreWallets$, + keystoreWallets$, // Transform `KeystoreWallets` -> `KeystoreWalletsUI` RxOp.map(FP.flow(A.map(({ id, name, selected }) => ({ id, name, selected })))), RxOp.shareReplay(1) ) -const id = FP.pipe(getKeystoreState(), getKeystoreId) +const id = FP.pipe(keystoreState(), getKeystoreId) if (!id) { throw Error(`Can't export keystore - keystore id is missing in KeystoreState`) } @@ -328,9 +342,9 @@ if (!id) { const validatePassword$ = (password: string): ValidatePasswordLD => password ? FP.pipe( - getKeystoreState(), + keystoreState(), getKeystoreId, - O.chain((id) => FP.pipe(getKeystoreWallets(), getKeystore(id))), + O.chain((id) => FP.pipe(keystoreWallets(), getKeystore(id))), O.fold( () => Rx.of(RD.failure(Error('Could not get current keystore to validate password'))), (keystore) => @@ -346,7 +360,7 @@ const validatePassword$ = (password: string): ValidatePasswordLD => : Rx.of(RD.initial) export const keystoreService: KeystoreService = { - keystore$: getKeystoreState$, + keystoreState$, addKeystoreWallet, removeKeystoreWallet, changeKeystoreWallet, @@ -357,13 +371,14 @@ export const keystoreService: KeystoreService = { lock, unlock, validatePassword$, - reloadKeystoreWallets, + keystoreWalletsPersistent$, + reloadPersistentKeystoreWallets, keystoreWalletsUI$, - keystoreWallets$, importingKeystoreState$, resetImportingKeystoreState: () => setImportingKeystoreState(RD.initial) } // TODO(@Veado) Remove it - for debugging only -getKeystoreState$.subscribe((v) => console.log('keystoreState subscription', v)) -getKeystoreWallets$.subscribe((v) => console.log('keystoreWallets subscription', v)) +keystoreState$.subscribe((v) => console.log('keystoreState sub', v)) +keystoreWallets$.subscribe((v) => console.log('keystoreWallets sub', v)) +keystoreWalletsUI$.subscribe((v) => console.log('keystoreWalletsUI$ sub', v)) diff --git a/src/renderer/services/wallet/ledger.ts b/src/renderer/services/wallet/ledger.ts index fbd34a91a..22375cb14 100644 --- a/src/renderer/services/wallet/ledger.ts +++ b/src/renderer/services/wallet/ledger.ts @@ -1,7 +1,10 @@ import * as RD from '@devexperts/remote-data-ts' import { Chain } from '@xchainjs/xchain-util' +import * as A from 'fp-ts/lib/Array' import * as FP from 'fp-ts/lib/function' +import * as M from 'fp-ts/lib/Map' import * as O from 'fp-ts/lib/Option' +import * as N from 'fp-ts/number' import * as Rx from 'rxjs' import * as RxOp from 'rxjs/operators' @@ -21,11 +24,19 @@ import { VerifyLedgerAddressHandler, AskLedgerAddressesHandler, RemoveLedgerAddressHandler, - isKeystoreUnlocked + isKeystoreUnlocked, + KeystoreWalletsUI$, + RemovedKeystoreId$ } from './types' import { hasImportedKeystore } from './util' -export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ }): LedgerService => { +export const createLedgerService = ({ + keystore$, + wallets$ +}: { + keystore$: KeystoreState$ + wallets$: KeystoreWalletsUI$ +}): LedgerService => { // State of all Ledger addresses added to a keystore wallet const { get$: keystoreLedgerAddresses$, @@ -37,7 +48,25 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ } keystore$, // Check unlocked keystore only RxOp.map(FP.flow(O.chain(O.fromPredicate(isKeystoreUnlocked)))), - RxOp.map(FP.flow(O.map(({ id }) => id))) + RxOp.map(FP.flow(O.map(({ id }) => id))), + RxOp.distinctUntilChanged(), + RxOp.shareReplay(1) + ) + + /** + * Stream of removed `KeystoreId`s + * by comparing changes of `KeystoreWalletsUI[]` + */ + const removedKeystoreId$: RemovedKeystoreId$ = FP.pipe( + wallets$, + // get prev. + curr. `KeystoreWalletsUI[]` + RxOp.pairwise(), + // Transform pair of `KeystoreWalletsUI[]` to pair of `KeystoreId[]` + RxOp.map((pair) => FP.pipe(pair, A.map(FP.flow(A.map(({ id }) => id))))), + RxOp.map(([prev, curr]) => + // get's the difference + FP.pipe(prev, A.difference(N.Eq)(curr), A.head) + ) ) const ledgerAddresses = (id: KeystoreId): LedgerAddressesMap => @@ -62,7 +91,8 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ } ) ) ) - ) + ), + RxOp.startWith(INITIAL_LEDGER_ADDRESSES_MAP) ) const setLedgerAddresses = (id: KeystoreId, addressesMap: LedgerAddressesMap) => { @@ -153,6 +183,19 @@ export const createLedgerService = ({ keystore$ }: { keystore$: KeystoreState$ } } }) + // Whenever a keystore have been removed, remove its related ledger addresses + removedKeystoreId$.subscribe((oKeystoreId: O.Option) => + FP.pipe( + oKeystoreId, + O.map((id) => FP.pipe(keystoreLedgerAddresses(), M.deleteAt(N.Eq)(id), setKeystoreLedgerAddresses)) + ) + ) + + // TODO(@Veado) Remove it - for debugging only + ledgerAddresses$.subscribe((v) => console.log('ledgerAddresses$ sub', v)) + keystoreLedgerAddresses$.subscribe((v) => console.log('keystoreLedgerAddresses$ sub', v)) + removedKeystoreId$.subscribe((v) => console.log('removedKeystoreId$ sub', v)) + return { ledgerAddresses$, askLedgerAddress$, diff --git a/src/renderer/services/wallet/types.ts b/src/renderer/services/wallet/types.ts index 25fe5c12c..e6d8667fc 100644 --- a/src/renderer/services/wallet/types.ts +++ b/src/renderer/services/wallet/types.ts @@ -58,6 +58,8 @@ export type ImportingKeystoreStateLD = Rx.Observable export type RemoveKeystoreWalletHandler = () => Promise +export type RemovedKeystoreId$ = Rx.Observable> + export type RenameKeystoreWalletRD = RD.RemoteData export type RenameKeystoreWalletLD = LiveData export type RenameKeystoreWalletHandler = (id: KeystoreId, name: string) => RenameKeystoreWalletLD @@ -67,7 +69,7 @@ export type ChangeKeystoreWalletLD = LiveData export type ChangeKeystoreWalletHandler = (id: KeystoreId) => ChangeKeystoreWalletLD export type KeystoreService = { - keystore$: KeystoreState$ + keystoreState$: KeystoreState$ addKeystoreWallet: (params: AddKeystoreParams) => Promise removeKeystoreWallet: RemoveKeystoreWalletHandler changeKeystoreWallet: ChangeKeystoreWalletHandler @@ -82,8 +84,8 @@ export type KeystoreService = { * No need to store any success data. Only status */ validatePassword$: ValidatePasswordHandler - reloadKeystoreWallets: FP.Lazy - keystoreWallets$: KeystoreWalletsLD + reloadPersistentKeystoreWallets: FP.Lazy + keystoreWalletsPersistent$: KeystoreWalletsLD keystoreWalletsUI$: KeystoreWalletsUI$ importingKeystoreState$: ImportingKeystoreStateLD resetImportingKeystoreState: FP.Lazy @@ -247,6 +249,7 @@ export type KeystoreLedgerAddressesMap = Map export type KeystoreWalletsRD = RD.RemoteData export type KeystoreWalletsLD = LiveData +export type KeystoreWallets$ = Rx.Observable export type KeystoreWalletUI = Omit export type KeystoreWalletsUI = KeystoreWalletUI[] diff --git a/src/renderer/views/app/AppView.tsx b/src/renderer/views/app/AppView.tsx index 54752163d..1c1fe3c72 100644 --- a/src/renderer/views/app/AppView.tsx +++ b/src/renderer/views/app/AppView.tsx @@ -72,7 +72,7 @@ export const AppView: React.FC = (): JSX.Element => { const prevHaltedChains = useRef([]) const prevMimirHalt = useRef(DEFAULT_MIMIR_HALT) - const { wallets: keystoreWallets, reload: reloadKeystoreWallets } = useKeystoreWallets() + const { walletsPersistentRD, reload: reloadPersistentWallets } = useKeystoreWallets() const { mimirHaltRD } = useMimirHalt() @@ -273,7 +273,7 @@ export const AppView: React.FC = (): JSX.Element => { {' '} keystoreWallets: {FP.pipe( - keystoreWallets, + walletsPersistentRD, RD.fold( () => <>init, () => <>loading, @@ -282,10 +282,10 @@ export const AppView: React.FC = (): JSX.Element => { ) )} - + ) - }, [keystoreWallets, reloadKeystoreWallets]) + }, [walletsPersistentRD, reloadPersistentWallets]) return ( diff --git a/src/renderer/views/deposit/DepositView.tsx b/src/renderer/views/deposit/DepositView.tsx index 69883c390..685e70c95 100644 --- a/src/renderer/views/deposit/DepositView.tsx +++ b/src/renderer/views/deposit/DepositView.tsx @@ -145,7 +145,7 @@ export const DepositView: React.FC = () => { // Because `useObservableState` will set its state NOT before first rendering loop, // and `AddWallet` would be rendered for the first time, // before a check of `keystoreState` can be done - const keystoreState = useObservableState(keystoreService.keystore$, undefined) + const keystoreState = useObservableState(keystoreService.keystoreState$, undefined) const poolDetailRD = useObservableState(selectedPoolDetail$, RD.initial) diff --git a/src/renderer/views/swap/SwapView.tsx b/src/renderer/views/swap/SwapView.tsx index 839fe11fc..c9acf1c70 100644 --- a/src/renderer/views/swap/SwapView.tsx +++ b/src/renderer/views/swap/SwapView.tsx @@ -63,7 +63,7 @@ const SuccessRouteView: React.FC = ({ sourceAsset, targetAsset }): JSX.El balancesState$, reloadBalancesByChain, getLedgerAddress$, - keystoreService: { keystore$, validatePassword$ } + keystoreService: { keystoreState$, validatePassword$ } } = useWalletContext() const [haltedChains] = useObservableState(() => FP.pipe(haltedChains$, RxOp.map(RD.getOrElse((): Chain[] => []))), []) @@ -71,7 +71,7 @@ const SuccessRouteView: React.FC = ({ sourceAsset, targetAsset }): JSX.El const { reloadApproveFee, approveFee$, approveERC20Token$, isApprovedERC20Token$ } = useEthereumContext() - const keystore = useObservableState(keystore$, O.none) + const keystore = useObservableState(keystoreState$, O.none) const poolsState = useObservableState(poolsState$, RD.initial) diff --git a/src/renderer/views/wallet/WalletAuth.tsx b/src/renderer/views/wallet/WalletAuth.tsx index 9f11d7ad8..f06ab2260 100644 --- a/src/renderer/views/wallet/WalletAuth.tsx +++ b/src/renderer/views/wallet/WalletAuth.tsx @@ -18,7 +18,7 @@ export const WalletAuth = ({ children }: { children: JSX.Element }): JSX.Element // Since `useObservableState` is set after first render (but not before) // and Route.render is called before first render, // we have to add 'undefined' as default value - const keystore = useObservableState(keystoreService.keystore$, undefined) + const keystore = useObservableState(keystoreService.keystoreState$, undefined) // Redirect if an user has not a phrase imported or wallet has been locked // Special case: keystore can be `undefined` (see comment at its definition using `useObservableState`) diff --git a/src/renderer/views/wallet/WalletSettingsAuth.tsx b/src/renderer/views/wallet/WalletSettingsAuth.tsx index be376fc45..41340614b 100644 --- a/src/renderer/views/wallet/WalletSettingsAuth.tsx +++ b/src/renderer/views/wallet/WalletSettingsAuth.tsx @@ -2,20 +2,29 @@ import React, { useCallback } from 'react' import * as FP from 'fp-ts/function' import * as O from 'fp-ts/lib/Option' +import { useObservableState } from 'observable-hooks' import { useLocation, useNavigate } from 'react-router-dom' +import * as RxOp from 'rxjs/operators' import { UnlockWalletSettings } from '../../components/settings' +import { useWalletContext } from '../../contexts/WalletContext' import { useCollapsedSetting } from '../../hooks/useCollapsedSetting' -import { useKeystoreState } from '../../hooks/useKeystoreState' import * as walletRoutes from '../../routes/wallet' +import { INITIAL_KEYSTORE_STATE } from '../../services/wallet/const' import { isKeystoreUnlocked } from '../../services/wallet/types' import { WalletSettingsView } from './WalletSettingsView' export const WalletSettingsAuth: React.FC = (): JSX.Element => { const navigate = useNavigate() const location = useLocation() + const { + keystoreService: { keystoreState$ } + } = useWalletContext() - const { state: keystoreState } = useKeystoreState() + // Note: Short delay for acting changes of `KeystoreState` is needed + // Just to let `WalletSettingsView` process changes w/o race conditions + // In other case it will jump to `UnlockWalletSettings` right after changing a wallet in `WalletSettingsView` + const keystoreState = useObservableState(FP.pipe(keystoreState$, RxOp.delay(100)), INITIAL_KEYSTORE_STATE) const { collapsed, toggle: toggleCollapse } = useCollapsedSetting('wallet') diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 8e86f6afd..5f6790899 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -37,7 +37,7 @@ export type IPCExportKeystoreParams = { fileName: string; keystore: Keystore } export type IPCSaveKeystoreParams = { id: KeystoreId; keystore: Keystore } export type ApiKeystore = { - saveKeystoreWallets: (wallets: KeystoreWallets) => Promise + saveKeystoreWallets: (wallets: KeystoreWallets) => Promise> exportKeystore: (params: IPCExportKeystoreParams) => Promise initKeystoreWallets: () => Promise> load: () => Promise diff --git a/src/shared/mock/api.ts b/src/shared/mock/api.ts index 41fb3acee..08d46e18a 100644 --- a/src/shared/mock/api.ts +++ b/src/shared/mock/api.ts @@ -8,7 +8,7 @@ import { MOCK_KEYSTORE } from './wallet' // Mock "empty" `apiKeystore` export const apiKeystore: ApiKeystore = { - saveKeystoreWallets: (_) => Promise.resolve(), + saveKeystoreWallets: (_) => Promise.resolve(E.right([])), exportKeystore: (_: IPCExportKeystoreParams) => Promise.resolve(), load: () => Promise.resolve(MOCK_KEYSTORE), initKeystoreWallets: () => Promise.resolve(E.right([]))