From d026c1969c8f5802ec44f7a5565e4a5ae2788e53 Mon Sep 17 00:00:00 2001 From: "@greweb" Date: Thu, 18 Jul 2024 17:48:51 +0200 Subject: [PATCH] LLD integration of Wallet Sync (#7300) chore: kickoff LLD integration of wallet sync --- apps/ledger-live-desktop/src/main/db/index.ts | 1 + .../components/WalletSyncContext.tsx | 17 ++ .../WalletSync/hooks/useDestroyTrustchain.ts | 14 +- .../hooks/useOnTrustchainRefreshNeeded.ts | 29 ++ .../WalletSync/hooks/useTrustchainSdk.ts | 17 +- .../WalletSync/hooks/useWatchWalletSync.ts | 160 +++++++++++ .../src/renderer/Default.tsx | 269 +++++++++--------- .../src/renderer/actions/accounts.ts | 5 + .../src/renderer/actions/trustchain.ts | 11 + .../src/renderer/actions/wallet.ts | 12 +- .../src/renderer/bridge/cache.ts | 2 +- .../src/renderer/components/IsUnlocked.tsx | 4 +- .../components/TopBar/ActivityIndicator.tsx | 15 +- .../ledger-live-desktop/src/renderer/init.tsx | 10 +- .../src/renderer/middlewares/db.ts | 11 +- .../src/renderer/reducers/accounts.ts | 2 + .../src/renderer/reducers/trustchain.ts | 11 - .../src/renderer/reducers/wallet.ts | 6 + .../src/renderer/storage.ts | 2 + apps/web-tools/trustchain/components/App.tsx | 6 +- .../trustchain/components/AppAccountsSync.tsx | 11 +- libs/live-wallet/src/ordering.test.ts | 2 +- libs/live-wallet/src/store.test.ts | 16 +- libs/live-wallet/src/store.ts | 16 +- 24 files changed, 467 insertions(+), 182 deletions(-) create mode 100644 apps/ledger-live-desktop/src/newArch/features/WalletSync/components/WalletSyncContext.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useOnTrustchainRefreshNeeded.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWatchWalletSync.ts create mode 100644 apps/ledger-live-desktop/src/renderer/actions/trustchain.ts diff --git a/apps/ledger-live-desktop/src/main/db/index.ts b/apps/ledger-live-desktop/src/main/db/index.ts index 4e9c4c81e363..c810e1d4c34c 100644 --- a/apps/ledger-live-desktop/src/main/db/index.ts +++ b/apps/ledger-live-desktop/src/main/db/index.ts @@ -92,6 +92,7 @@ async function reload() { const encryptedDataPaths = [ ["app", "accounts"], ["app", "trustchain"], + ["app", "wallet"], ]; /** diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/WalletSyncContext.tsx b/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/WalletSyncContext.tsx new file mode 100644 index 000000000000..0910b080f7ea --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/components/WalletSyncContext.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { useWatchWalletSync, WalletSyncUserState } from "../hooks/useWatchWalletSync"; + +export const WalletSyncContext = React.createContext({ + visualPending: false, + walletSyncError: null, + onUserRefresh: () => {}, +}); + +export const useWalletSyncUserState = () => React.useContext(WalletSyncContext); + +export function WalletSyncProvider({ children }: { children: React.ReactNode }) { + const walletSyncState = useWatchWalletSync(); + return ( + {children} + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useDestroyTrustchain.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useDestroyTrustchain.ts index 29b966f65ab1..27e3d5d642dd 100644 --- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useDestroyTrustchain.ts +++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useDestroyTrustchain.ts @@ -6,24 +6,32 @@ import { memberCredentialsSelector, } from "@ledgerhq/trustchain/store"; import { useMutation } from "@tanstack/react-query"; -import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types"; import { setFlow } from "~/renderer/actions/walletSync"; import { Flow, Step } from "~/renderer/reducers/walletSync"; import { QueryKey } from "./type.hooks"; +import { useCloudSyncSDK } from "./useWatchWalletSync"; +import { walletSyncUpdate } from "@ledgerhq/live-wallet/store"; export function useDestroyTrustchain() { const dispatch = useDispatch(); const sdk = useTrustchainSdk(); + const cloudSyncSDK = useCloudSyncSDK(); const trustchain = useSelector(trustchainSelector); const memberCredentials = useSelector(memberCredentialsSelector); const deleteMutation = useMutation({ - mutationFn: () => - sdk.destroyTrustchain(trustchain as Trustchain, memberCredentials as MemberCredentials), + mutationFn: async () => { + if (!trustchain || !memberCredentials) { + return; + } + await cloudSyncSDK.destroy(trustchain, memberCredentials); + await sdk.destroyTrustchain(trustchain, memberCredentials); + }, mutationKey: [QueryKey.destroyTrustchain, trustchain], onSuccess: () => { dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeleted })); dispatch(resetTrustchainStore()); + dispatch(walletSyncUpdate(null, 0)); }, onError: () => dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeletionError })), }); diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useOnTrustchainRefreshNeeded.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useOnTrustchainRefreshNeeded.ts new file mode 100644 index 000000000000..ac160692db4d --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useOnTrustchainRefreshNeeded.ts @@ -0,0 +1,29 @@ +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { MemberCredentials, Trustchain, TrustchainSDK } from "@ledgerhq/trustchain/types"; +import { setTrustchain, resetTrustchainStore } from "@ledgerhq/trustchain/store"; +import { TrustchainEjected } from "@ledgerhq/trustchain/errors"; +import { log } from "@ledgerhq/logs"; + +export function useOnTrustchainRefreshNeeded( + trustchainSdk: TrustchainSDK, + memberCredentials: MemberCredentials | null, +): (trustchain: Trustchain) => Promise { + const dispatch = useDispatch(); + const onTrustchainRefreshNeeded = useCallback( + async (trustchain: Trustchain) => { + try { + if (!memberCredentials) return; + log("walletsync", "onTrustchainRefreshNeeded " + trustchain.rootId); + const newTrustchain = await trustchainSdk.restoreTrustchain(trustchain, memberCredentials); + setTrustchain(newTrustchain); + } catch (e) { + if (e instanceof TrustchainEjected) { + dispatch(resetTrustchainStore()); + } + } + }, + [dispatch, trustchainSdk, memberCredentials], + ); + return onTrustchainRefreshNeeded; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts index 717ada7e52e1..4ea87e143899 100644 --- a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts +++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts @@ -5,6 +5,10 @@ import { getEnv } from "@ledgerhq/live-env"; import { getSdk } from "@ledgerhq/trustchain/index"; import { withDevice } from "@ledgerhq/live-common/hw/deviceAccess"; import Transport from "@ledgerhq/hw-transport"; +import { trustchainLifecycle } from "@ledgerhq/live-wallet/walletsync/index"; +import { useStore } from "react-redux"; +import { walletSelector } from "~/renderer/reducers/wallet"; +import { walletSyncStateSelector } from "@ledgerhq/live-wallet/store"; export function runWithDevice( deviceId: string | undefined, @@ -28,7 +32,18 @@ export function useTrustchainSdk() { const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`; return { applicationId, name }; }, []); - const sdk = getSdk(isMockEnv, defaultContext); + const store = useStore(); + const lifecycle = useMemo( + () => + trustchainLifecycle({ + getCurrentWSState: () => walletSyncStateSelector(walletSelector(store.getState())), + }), + [store], + ); + const sdk = useMemo( + () => getSdk(isMockEnv, defaultContext, lifecycle), + [isMockEnv, defaultContext, lifecycle], + ); return sdk; } diff --git a/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWatchWalletSync.ts b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWatchWalletSync.ts new file mode 100644 index 000000000000..7a152c12e6bd --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useWatchWalletSync.ts @@ -0,0 +1,160 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useDispatch, useSelector, useStore } from "react-redux"; +import noop from "lodash/noop"; +import { CloudSyncSDK, UpdateEvent } from "@ledgerhq/live-wallet/cloudsync/index"; +import walletsync, { + liveSlug, + DistantState, + walletSyncWatchLoop, + LocalState, + Schema, +} from "@ledgerhq/live-wallet/walletsync/index"; +import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; +import { walletSelector } from "~/renderer/reducers/wallet"; +import { memberCredentialsSelector, trustchainSelector } from "@ledgerhq/trustchain/store"; +import { State } from "~/renderer/reducers"; +import { cache as bridgeCache } from "~/renderer/bridge/cache"; +import { + setAccountNames, + walletSyncStateSelector, + walletSyncUpdate, +} from "@ledgerhq/live-wallet/store"; +import { replaceAccounts } from "~/renderer/actions/accounts"; +import { latestDistantStateSelector } from "~/renderer/reducers/wallet"; +import { log } from "@ledgerhq/logs"; +import { useTrustchainSdk } from "./useTrustchainSdk"; +import { useOnTrustchainRefreshNeeded } from "./useOnTrustchainRefreshNeeded"; +import { Dispatch } from "redux"; + +function localStateSelector(state: State): LocalState { + // READ. connect the redux state to the walletsync modules + return { + accounts: { list: state.accounts }, + accountNames: state.wallet.accountNames, + }; +} + +function saveUpdate(newLocalState: LocalState, dispatch: Dispatch) { + // WRITE. save the state for the walletsync modules + dispatch(setAccountNames(newLocalState.accountNames)); + dispatch(replaceAccounts(newLocalState.accounts.list)); // IMPORTANT: keep this one last, it's doing the DB:* trigger to save the data +} + +export function useCloudSyncSDK(): CloudSyncSDK { + const trustchainSdk = useTrustchainSdk(); + const store = useStore(); + const dispatch = useDispatch(); + const getCurrentVersion = useCallback( + () => walletSyncStateSelector(walletSelector(store.getState())).version, + [store], + ); + + const saveNewUpdate = useCallback( + async (event: UpdateEvent) => { + log("walletsync", "saveNewUpdate", { event }); + switch (event.type) { + case "new-data": { + // we resolve incoming distant state changes + const ctx = { getAccountBridge, bridgeCache, blacklistedTokenIds: [] }; + const state = store.getState(); + const latest = latestDistantStateSelector(state); + const local = localStateSelector(state); + const data = event.data; + const resolved = await walletsync.resolveIncomingDistantState(ctx, local, latest, data); + + if (resolved.hasChanges) { + const version = event.version; + const localState = localStateSelector(store.getState()); // fetch again latest state because it might have changed + const newLocalState = walletsync.applyUpdate(localState, resolved.update); // we resolve in sync the new local state to save + dispatch(walletSyncUpdate(data, version)); + saveUpdate(newLocalState, dispatch); + log("walletsync", "resolved. changes applied."); + } else { + log("walletsync", "resolved. no changes to apply."); + } + break; + } + case "pushed-data": { + dispatch(walletSyncUpdate(event.data, event.version)); + break; + } + case "deleted-data": { + dispatch(walletSyncUpdate(null, 0)); + break; + } + } + }, + [store, dispatch], + ); + + const cloudSyncSDK = useMemo( + () => + new CloudSyncSDK({ + slug: liveSlug, + schema: walletsync.schema, + trustchainSdk, + getCurrentVersion, + saveNewUpdate, + }), + [trustchainSdk, getCurrentVersion, saveNewUpdate], + ); + + return cloudSyncSDK; +} + +export type WalletSyncUserState = { + visualPending: boolean; + walletSyncError: Error | null; + onUserRefresh: () => void; +}; + +export function useWatchWalletSync(): WalletSyncUserState { + const store = useStore(); + const memberCredentials = useSelector(memberCredentialsSelector); + const trustchain = useSelector(trustchainSelector); + const trustchainSdk = useTrustchainSdk(); + const walletSyncSdk = useCloudSyncSDK(); + const onTrustchainRefreshNeeded = useOnTrustchainRefreshNeeded(trustchainSdk, memberCredentials); + + const [visualPending, setVisualPending] = useState(true); + const [walletSyncError, setWalletSyncError] = useState(null); + const [onUserRefresh, setOnUserRefresh] = useState<() => void>(() => noop); + const state = useMemo( + () => ({ visualPending, walletSyncError, onUserRefresh }), + [visualPending, walletSyncError, onUserRefresh], + ); + + // pull and push wallet sync loop + useEffect(() => { + if (!trustchain || !memberCredentials) { + setOnUserRefresh(() => noop); + return; + } + + const { unsubscribe, onUserRefreshIntent } = walletSyncWatchLoop({ + walletSyncSdk, + trustchain, + memberCredentials, + setVisualPending, + getState: () => store.getState(), + localStateSelector, + latestDistantStateSelector, + onError: e => setWalletSyncError(e && e instanceof Error ? e : new Error(String(e))), + onStartPolling: () => setWalletSyncError(null), + onTrustchainRefreshNeeded, + }); + + setOnUserRefresh(() => onUserRefreshIntent); + + return unsubscribe; + }, [ + store, + trustchainSdk, + walletSyncSdk, + trustchain, + memberCredentials, + onTrustchainRefreshNeeded, + ]); + + return state; +} diff --git a/apps/ledger-live-desktop/src/renderer/Default.tsx b/apps/ledger-live-desktop/src/renderer/Default.tsx index c98fc784d20e..12a51939abe5 100644 --- a/apps/ledger-live-desktop/src/renderer/Default.tsx +++ b/apps/ledger-live-desktop/src/renderer/Default.tsx @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from "react-redux"; import TrackAppStart from "~/renderer/components/TrackAppStart"; import { LiveApp } from "~/renderer/screens/platform"; import { BridgeSyncProvider } from "~/renderer/bridge/BridgeSyncContext"; +import { WalletSyncProvider } from "~/newArch/features/WalletSync/components/WalletSyncContext"; import { SyncNewAccounts } from "~/renderer/bridge/SyncNewAccounts"; import Box from "~/renderer/components/Box/Box"; import { useListenToHidDevices } from "./hooks/useListenToHidDevices"; @@ -244,140 +245,152 @@ export default function Default() { - - - - {process.env.DEBUG_THEME ? : null} - {process.env.MOCK ? : null} - {process.env.DEBUG_UPDATE ? : null} - {process.env.DEBUG_SKELETONS ? : null} - {process.env.DEBUG_FIRMWARE_UPDATE ? : null} - - {process.env.DISABLE_TRANSACTION_BROADCAST ? ( - - ) : null} - - ( - <> - }> - - - - - )} - /> - - ( - <> - }> - - - - - )} - /> - + + + + + {process.env.DEBUG_THEME ? : null} + {process.env.MOCK ? : null} + {process.env.DEBUG_UPDATE ? : null} + {process.env.DEBUG_SKELETONS ? : null} + {process.env.DEBUG_FIRMWARE_UPDATE ? : null} + + {process.env.DISABLE_TRANSACTION_BROADCAST ? ( + + ) : null} + + ( + <> + }> + + + + + )} + /> + + ( + <> + }> + + + + + )} + /> + - - }> - - - + + }> + + + - {!hasCompletedOnboarding ? ( - - - - - - - ) : ( - + {!hasCompletedOnboarding ? ( - - - - - + + + + + + ) : ( + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - {__PRERELEASE__ && __CHANNEL__ !== "next" && !__CHANNEL__.includes("sha") ? ( - - ) : null} + {__PRERELEASE__ && + __CHANNEL__ !== "next" && + !__CHANNEL__.includes("sha") ? ( + + ) : null} - - - - - - - - - - - - - )} - - + + + + + + + + + + + + + )} + + + diff --git a/apps/ledger-live-desktop/src/renderer/actions/accounts.ts b/apps/ledger-live-desktop/src/renderer/actions/accounts.ts index 15685766de9d..ca53f82c2e93 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/accounts.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/accounts.ts @@ -21,6 +21,11 @@ export const initAccounts = (data: [Account, AccountUserData][]) => { }; }; +export const replaceAccounts = (accounts: Account[]) => ({ + type: "DB:REPLACE_ACCOUNTS", + payload: accounts, +}); + export const reorderAccounts = (comparator: AccountComparator) => (dispatch: Dispatch) => dispatch({ type: "DB:REORDER_ACCOUNTS", diff --git a/apps/ledger-live-desktop/src/renderer/actions/trustchain.ts b/apps/ledger-live-desktop/src/renderer/actions/trustchain.ts new file mode 100644 index 000000000000..24f58b5ed611 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/actions/trustchain.ts @@ -0,0 +1,11 @@ +import { Dispatch } from "redux"; +import { importTrustchainStoreState } from "@ledgerhq/trustchain/store"; +import { getKey } from "~/renderer/storage"; + +export const fetchTrustchain = () => async (dispatch: Dispatch) => { + const data = await getKey("app", "trustchain"); + if (data && typeof data === "object") { + // we don't thorw in this case, only accounts is used as password check safeguard + return dispatch(importTrustchainStoreState(data)); + } +}; diff --git a/apps/ledger-live-desktop/src/renderer/actions/wallet.ts b/apps/ledger-live-desktop/src/renderer/actions/wallet.ts index 806628353d7b..d21173264aa8 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/wallet.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/wallet.ts @@ -1,7 +1,17 @@ -import { setAccountStarred } from "@ledgerhq/live-wallet/store"; +import { importWalletState, setAccountStarred } from "@ledgerhq/live-wallet/store"; +import { getKey } from "../storage"; +import { Dispatch } from "redux"; export const toggleStarAction = (id: string, value: boolean) => { const action = setAccountStarred(id, value); action.type = "DB:" + action.type; return action; }; + +export const fetchWallet = () => async (dispatch: Dispatch) => { + const data = await getKey("app", "wallet"); + if (data && data.walletSyncState) { + // we don't throw in this case, only accounts is used as password check safeguard + dispatch(importWalletState(data)); + } +}; diff --git a/apps/ledger-live-desktop/src/renderer/bridge/cache.ts b/apps/ledger-live-desktop/src/renderer/bridge/cache.ts index 433da54f5493..aa1cd9d02476 100644 --- a/apps/ledger-live-desktop/src/renderer/bridge/cache.ts +++ b/apps/ledger-live-desktop/src/renderer/bridge/cache.ts @@ -35,7 +35,7 @@ export function getCurrencyCache(currency: CryptoCurrency): unknown { } return undefined; } -const cache = makeBridgeCacheSystem({ +export const cache = makeBridgeCacheSystem({ saveData(c, d) { setCurrencyCache(c, d); return Promise.resolve(); diff --git a/apps/ledger-live-desktop/src/renderer/components/IsUnlocked.tsx b/apps/ledger-live-desktop/src/renderer/components/IsUnlocked.tsx index c0bfdcfd7761..1b178661b36d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/IsUnlocked.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/IsUnlocked.tsx @@ -7,7 +7,7 @@ import { setEncryptionKey, isEncryptionKeyCorrect, hasBeenDecrypted } from "~/re import IconTriangleWarning from "~/renderer/icons/TriangleWarning"; import { useHardReset } from "~/renderer/reset"; import { fetchAccounts } from "~/renderer/actions/accounts"; -import { fetchTrustchain } from "~/renderer/reducers/trustchain"; +import { fetchTrustchain } from "~/renderer/actions/trustchain"; import { unlock } from "~/renderer/actions/application"; import { isLocked as isLockedSelector } from "~/renderer/reducers/application"; import Box from "~/renderer/components/Box"; @@ -17,6 +17,7 @@ import Button from "~/renderer/components/Button"; import ConfirmModal from "~/renderer/modals/ConfirmModal"; import IconArrowRight from "~/renderer/icons/ArrowRight"; import Logo from "~/renderer/icons/Logo"; +import { fetchWallet } from "../actions/wallet"; export default function IsUnlocked({ children }: { children: React.ReactNode }): JSX.Element { const dispatch = useDispatch(); @@ -52,6 +53,7 @@ export default function IsUnlocked({ children }: { children: React.ReactNode }): await setEncryptionKey(inputValue.password); await dispatch(fetchAccounts()); await dispatch(fetchTrustchain()); + await dispatch(fetchWallet()); } else if (!(await isEncryptionKeyCorrect(inputValue.password))) { throw new PasswordIncorrectError(); } diff --git a/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx b/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx index 618c87e50cfe..ea0859a1d4f4 100644 --- a/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/TopBar/ActivityIndicator.tsx @@ -14,18 +14,23 @@ import Tooltip from "../Tooltip"; import TranslatedError from "../TranslatedError"; import Box from "../Box"; import { ItemContainer } from "./shared"; +import { useWalletSyncUserState } from "~/newArch/features/WalletSync/components/WalletSyncContext"; + export default function ActivityIndicatorInner() { + const wsUserState = useWalletSyncUserState(); const bridgeSync = useBridgeSync(); const globalSyncState = useGlobalSyncState(); const isUpToDate = useSelector(isUpToDateSelector); const cvPolling = useCountervaluesPolling(); - const isPending = cvPolling.pending || globalSyncState.pending; - const syncError = !isPending && (cvPolling.error || globalSyncState.error); + const isPending = cvPolling.pending || globalSyncState.pending || wsUserState.visualPending; + const syncError = + !isPending && (cvPolling.error || globalSyncState.error || wsUserState.walletSyncError); // we only show error if it's not up to date. this hide a bit error that happen from time to time - const isError = !!syncError && !isUpToDate; - const error = syncError ? globalSyncState.error : null; + const isError = (!!syncError && !isUpToDate) || !!wsUserState.walletSyncError; + const error = (syncError ? globalSyncState.error : null) || wsUserState.walletSyncError; const [lastClickTime, setLastclickTime] = useState(0); const onClick = useCallback(() => { + wsUserState.onUserRefresh(); cvPolling.poll(); bridgeSync({ type: "SYNC_ALL_ACCOUNTS", @@ -34,7 +39,7 @@ export default function ActivityIndicatorInner() { }); setLastclickTime(Date.now()); track("SyncRefreshClick"); - }, [cvPolling, bridgeSync]); + }, [cvPolling, bridgeSync, wsUserState]); const isSpectronRun = getEnv("PLAYWRIGHT_RUN"); // we will keep 'spinning' in spectron case const userClickTime = isSpectronRun ? 10000 : 1000; const isUserClick = Date.now() - lastClickTime < userClickTime; // time to keep display the spinning on a UI click. diff --git a/apps/ledger-live-desktop/src/renderer/init.tsx b/apps/ledger-live-desktop/src/renderer/init.tsx index 68cc1a24e75c..0359a85d24b3 100644 --- a/apps/ledger-live-desktop/src/renderer/init.tsx +++ b/apps/ledger-live-desktop/src/renderer/init.tsx @@ -47,8 +47,9 @@ import { addDevice, removeDevice, resetDevices } from "~/renderer/actions/device import { Device } from "@ledgerhq/live-common/hw/actions/types"; import { listCachedCurrencyIds } from "./bridge/cache"; import { LogEntry } from "winston"; -import { importTrustchainStoreState } from "@ledgerhq/trustchain/store"; import { importMarketState } from "./actions/market"; +import { fetchWallet } from "./actions/wallet"; +import { fetchTrustchain } from "./actions/trustchain"; const rootNode = document.getElementById("react-root"); const TAB_KEY = 9; @@ -180,10 +181,9 @@ async function init() { }), ); } - const trustchainStoreState = await getKey("app", "trustchain"); - if (trustchainStoreState) { - store.dispatch(importTrustchainStoreState(trustchainStoreState)); - } + + await fetchTrustchain()(store.dispatch); + await fetchWallet()(store.dispatch); const marketState = await getKey("app", "market"); if (marketState) { diff --git a/apps/ledger-live-desktop/src/renderer/middlewares/db.ts b/apps/ledger-live-desktop/src/renderer/middlewares/db.ts index 1058a8fda07a..1ead4004571d 100644 --- a/apps/ledger-live-desktop/src/renderer/middlewares/db.ts +++ b/apps/ledger-live-desktop/src/renderer/middlewares/db.ts @@ -7,8 +7,11 @@ import { actionTypePrefix as postOnboardingActionTypePrefix } from "@ledgerhq/li import { settingsExportSelector, areSettingsLoaded } from "./../reducers/settings"; import { State } from "../reducers"; import { Account, AccountUserData } from "@ledgerhq/types-live"; -import { accountUserDataExportSelector } from "@ledgerhq/live-wallet/store"; - +import { + accountUserDataExportSelector, + walletStateExportShouldDiffer, + exportWalletState, +} from "@ledgerhq/live-wallet/store"; import { trustchainStoreActionTypePrefix, trustchainStoreSelector, @@ -67,6 +70,10 @@ const DBMiddleware: Middleware<{}, State> = store => next => action => { setKey("app", "settings", settingsExportSelector(newState)); } } + + if (walletStateExportShouldDiffer(oldState.wallet, newState.wallet)) { + setKey("app", "wallet", exportWalletState(newState.wallet)); + } return res; } }; diff --git a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts index 0c465c5c2e7a..722ad096ec74 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts @@ -38,6 +38,7 @@ type HandlersPayloads = { REMOVE_ACCOUNT: Account; CLEAN_FULLNODE_DISCONNECT: never; CLEAN_ACCOUNTS_CACHE: never; + REPLACE_ACCOUNTS: Account[]; }; type AccountsHandlers = Handlers; @@ -56,6 +57,7 @@ const handlers: AccountsHandlers = { REMOVE_ACCOUNT: (state, { payload: account }) => state.filter(acc => acc.id !== account.id), CLEAN_FULLNODE_DISCONNECT: state => state.filter(acc => acc.currency.id !== "bitcoin"), CLEAN_ACCOUNTS_CACHE: state => state.map(clearAccount), + REPLACE_ACCOUNTS: (state, { payload }) => payload, }; export default handleActions( diff --git a/apps/ledger-live-desktop/src/renderer/reducers/trustchain.ts b/apps/ledger-live-desktop/src/renderer/reducers/trustchain.ts index e9dba0199a11..f8ba5a947da3 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/trustchain.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/trustchain.ts @@ -1,4 +1,3 @@ -import { Dispatch } from "redux"; import { getInitialStore, TrustchainStore, @@ -7,18 +6,8 @@ import { TrustchainHandlers, } from "@ledgerhq/trustchain/store"; import { handleActions } from "redux-actions"; -import { importTrustchainStoreState } from "@ledgerhq/trustchain/store"; -import { getKey } from "~/renderer/storage"; export default handleActions< TrustchainStore, TrustchainHandlersPayloads[keyof TrustchainHandlersPayloads] >(trustchainHandlers as unknown as TrustchainHandlers, getInitialStore()); - -export const fetchTrustchain = () => async (dispatch: Dispatch) => { - const data = await getKey("app", "trustchain"); - if (data) { - // we don't thorw in this case, only accounts is used as password check safeguard - return dispatch(importTrustchainStoreState(data)); - } -}; diff --git a/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts b/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts index 32520ca7fdef..66d95ac6f44f 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/wallet.ts @@ -6,7 +6,9 @@ import { handlers, isStarredAccountSelector, accountNameWithDefaultSelector, + walletSyncStateSelector, } from "@ledgerhq/live-wallet/store"; +import { DistantState } from "@ledgerhq/live-wallet/walletsync/index"; import { handleActions } from "redux-actions"; import { State } from "."; import { createSelector } from "reselect"; @@ -21,6 +23,10 @@ export const accountStarredSelector = createSelector( (wallet, accountId) => isStarredAccountSelector(wallet, { accountId }), ); +export function latestDistantStateSelector(state: State): DistantState | null { + return walletSyncStateSelector(walletSelector(state)).data; +} + export const useMaybeAccountName = ( account: AccountLike | null | undefined, ): string | undefined => { diff --git a/apps/ledger-live-desktop/src/renderer/storage.ts b/apps/ledger-live-desktop/src/renderer/storage.ts index 6f95ff8e7e05..714d05a6ea1e 100644 --- a/apps/ledger-live-desktop/src/renderer/storage.ts +++ b/apps/ledger-live-desktop/src/renderer/storage.ts @@ -24,6 +24,7 @@ import { settingsExportSelector } from "./reducers/settings"; import logger from "./logger"; import { trustchainStoreSelector } from "@ledgerhq/trustchain/store"; import { marketStoreSelector } from "./reducers/market"; +import { ExportedWalletState } from "@ledgerhq/live-wallet/store"; /* This file serve as an interface for the RPC binding to the main thread that now manage the config file. @@ -58,6 +59,7 @@ type DatabaseValues = { postOnboarding: PostOnboarding; settings: Settings; trustchain: TrustchainStore; + wallet: ExportedWalletState; market: Market; PLAYWRIGHT_RUN: { localStorage?: Record; diff --git a/apps/web-tools/trustchain/components/App.tsx b/apps/web-tools/trustchain/components/App.tsx index 79563ee9c265..af191f3e52a0 100644 --- a/apps/web-tools/trustchain/components/App.tsx +++ b/apps/web-tools/trustchain/components/App.tsx @@ -82,8 +82,8 @@ const App = () => { const [wssdkHandledVersion, setWssdkHandledVersion] = useState(0); const [wssdkHandledData, setWssdkHandledData] = useState(null); - const version = state.walletState.wsState.version || wssdkHandledVersion; - const data = state.walletState.wsState.data || wssdkHandledData; + const version = state.walletState.walletSyncState.version || wssdkHandledVersion; + const data = state.walletState.walletSyncState.data || wssdkHandledData; const wsStateRef = useRef({ version, data }); useEffect(() => { @@ -282,7 +282,7 @@ const App = () => { data={data} setVersion={setWssdkHandledVersion} setData={setWssdkHandledData} - forceReadOnlyData={state.walletState.wsState.data} + forceReadOnlyData={state.walletState.walletSyncState.data} readOnly={accountsSync} takeControl={takeControl} /> diff --git a/apps/web-tools/trustchain/components/AppAccountsSync.tsx b/apps/web-tools/trustchain/components/AppAccountsSync.tsx index 9c9dce4714f5..0e50602f51d6 100644 --- a/apps/web-tools/trustchain/components/AppAccountsSync.tsx +++ b/apps/web-tools/trustchain/components/AppAccountsSync.tsx @@ -46,7 +46,7 @@ const localStateSelector = (state: State) => ({ accountNames: state.walletState.accountNames, }); -const latestDistantStateSelector = (state: State) => state.walletState.wsState.data; +const latestDistantStateSelector = (state: State) => state.walletState.walletSyncState.data; export default function AppAccountsSync({ deviceId, @@ -71,7 +71,10 @@ export default function AppAccountsSync({ stateRef.current = state; }, [state]); - const getCurrentVersion = useCallback(() => stateRef.current.walletState.wsState.version, []); + const getCurrentVersion = useCallback( + () => stateRef.current.walletState.walletSyncState.version, + [], + ); // in memory implementation of bridgeCache const bridgeCache = useMemo(() => { @@ -96,7 +99,7 @@ export default function AppAccountsSync({ const state = stateRef.current; const version = event.version; const data = event.data; - const wsState = state.walletState.wsState; + const walletSyncState = state.walletState.walletSyncState; const localState = localStateSelector(state); const ctx = { getAccountBridge, bridgeCache, blacklistedTokenIds: [] }; @@ -104,7 +107,7 @@ export default function AppAccountsSync({ const resolved = await walletsync.resolveIncomingDistantState( ctx, localState, - wsState.data, + walletSyncState.data, data, ); diff --git a/libs/live-wallet/src/ordering.test.ts b/libs/live-wallet/src/ordering.test.ts index 0b8541f557bb..349f911100d6 100644 --- a/libs/live-wallet/src/ordering.test.ts +++ b/libs/live-wallet/src/ordering.test.ts @@ -90,7 +90,7 @@ const accounts = raws.map(a => fromAccountRaw(a)); const walletState: WalletState = { accountNames: new Map(), starredAccountIds: new Set(), - wsState: { + walletSyncState: { data: null, version: 0, }, diff --git a/libs/live-wallet/src/store.test.ts b/libs/live-wallet/src/store.test.ts index dc9755251394..11b54de45d98 100644 --- a/libs/live-wallet/src/store.test.ts +++ b/libs/live-wallet/src/store.test.ts @@ -15,7 +15,7 @@ import { setAccountStarred, walletStateExportShouldDiffer, walletSyncUpdate, - wsStateSelector, + walletSyncStateSelector, } from "./store"; import { genAccount } from "@ledgerhq/coin-framework/mocks/account"; import type { Account } from "@ledgerhq/types-live"; @@ -176,7 +176,7 @@ describe("Wallet store", () => { it("can update the wallet sync state", () => { const result = handlers.WALLET_SYNC_UPDATE(initialState, walletSyncUpdate({}, 42)); - expect(result.wsState).toEqual({ + expect(result.walletSyncState).toEqual({ data: {}, version: 42, }); @@ -184,29 +184,29 @@ describe("Wallet store", () => { it("can import the wallet state", () => { const exportedState = { - wsState: { data: {}, version: 42 }, + walletSyncState: { data: {}, version: 42 }, }; const result = handlers.IMPORT_WALLET_SYNC(initialState, importWalletState(exportedState)); - expect(wsStateSelector(result)).toEqual({ data: {}, version: 42 }); + expect(walletSyncStateSelector(result)).toEqual({ data: {}, version: 42 }); }); it("can export the wallet state", () => { const exportedState = { - wsState: { data: {}, version: 42 }, + walletSyncState: { data: {}, version: 42 }, }; const result = handlers.IMPORT_WALLET_SYNC(initialState, importWalletState(exportedState)); expect(exportWalletState(result)).toEqual({ - wsState: { data: {}, version: 42 }, + walletSyncState: { data: {}, version: 42 }, }); }); it("walletStateExportShouldDiffer", () => { const exportedState = { - wsState: { data: {}, version: 42 }, + walletSyncState: { data: {}, version: 42 }, }; const result = handlers.IMPORT_WALLET_SYNC(initialState, importWalletState(exportedState)); expect(exportWalletState(result)).toEqual({ - wsState: { data: {}, version: 42 }, + walletSyncState: { data: {}, version: 42 }, }); expect(walletStateExportShouldDiffer(initialState, result)).toBe(true); expect(walletStateExportShouldDiffer(initialState, initialState)).toBe(false); diff --git a/libs/live-wallet/src/store.ts b/libs/live-wallet/src/store.ts index d35c134d1588..30ebc7cf7484 100644 --- a/libs/live-wallet/src/store.ts +++ b/libs/live-wallet/src/store.ts @@ -22,17 +22,17 @@ export type WalletState = { starredAccountIds: Set; // local copy of the wallet sync data last synchronized with the backend of wallet sync, in order to be able to diff what we need to do when we apply an incremental update - wsState: WSState; + walletSyncState: WSState; }; export type ExportedWalletState = { - wsState: WSState; + walletSyncState: WSState; }; export const initialState: WalletState = { accountNames: new Map(), starredAccountIds: new Set(), - wsState: { data: null, version: 0 }, + walletSyncState: { data: null, version: 0 }, }; export enum WalletHandlerType { @@ -121,10 +121,10 @@ export const handlers: WalletHandlers = { return { ...state, accountNames }; }, WALLET_SYNC_UPDATE: (state, { payload }) => { - return { ...state, wsState: payload }; + return { ...state, walletSyncState: payload }; }, IMPORT_WALLET_SYNC: (state, { payload }) => { - return { ...state, wsState: payload.wsState }; + return { ...state, walletSyncState: payload.walletSyncState }; }, }; @@ -225,11 +225,11 @@ export const accountRawToAccountUserData = (raw: AccountRaw): AccountUserData => * call this selector to save the store state */ export const exportWalletState = (state: WalletState): ExportedWalletState => ({ - wsState: state.wsState, + walletSyncState: state.walletSyncState, }); export const walletStateExportShouldDiffer = (a: WalletState, b: WalletState): boolean => { - return a.wsState !== b.wsState; + return a.walletSyncState !== b.walletSyncState; }; -export const wsStateSelector = (state: WalletState): WSState => state.wsState; +export const walletSyncStateSelector = (state: WalletState): WSState => state.walletSyncState;