diff --git a/apps/ledger-live-desktop/src/main/db/index.ts b/apps/ledger-live-desktop/src/main/db/index.ts index 4e9c4c81e36..c810e1d4c34 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 00000000000..0910b080f7e --- /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 29b966f65ab..27e3d5d642d 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 00000000000..ac160692db4 --- /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 717ada7e52e..4ea87e14389 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 00000000000..7a152c12e6b --- /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 c98fc784d20..12a51939abe 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 15685766de9..ca53f82c2e9 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 00000000000..24f58b5ed61 --- /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 806628353d7..d21173264aa 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 433da54f549..aa1cd9d0247 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 c0bfdcfd776..1b178661b36 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 618c87e50cf..ea0859a1d4f 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 68cc1a24e75..0359a85d24b 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 1058a8fda07..1ead4004571 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 0c465c5c2e7..722ad096ec7 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 e9dba0199a1..f8ba5a947da 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 32520ca7fde..66d95ac6f44 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 6f95ff8e7e0..714d05a6ea1 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 79563ee9c26..af191f3e52a 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 9c9dce4714f..0e50602f51d 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 0b8541f557b..349f911100d 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 dc975525139..11b54de45d9 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 d35c134d158..30ebc7cf748 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;