Skip to content

Commit d026c19

Browse files
authored
LLD integration of Wallet Sync (#7300)
chore: kickoff LLD integration of wallet sync
1 parent 98297e5 commit d026c19

File tree

24 files changed

+467
-182
lines changed

24 files changed

+467
-182
lines changed

apps/ledger-live-desktop/src/main/db/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ async function reload() {
9292
const encryptedDataPaths = [
9393
["app", "accounts"],
9494
["app", "trustchain"],
95+
["app", "wallet"],
9596
];
9697

9798
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from "react";
2+
import { useWatchWalletSync, WalletSyncUserState } from "../hooks/useWatchWalletSync";
3+
4+
export const WalletSyncContext = React.createContext<WalletSyncUserState>({
5+
visualPending: false,
6+
walletSyncError: null,
7+
onUserRefresh: () => {},
8+
});
9+
10+
export const useWalletSyncUserState = () => React.useContext(WalletSyncContext);
11+
12+
export function WalletSyncProvider({ children }: { children: React.ReactNode }) {
13+
const walletSyncState = useWatchWalletSync();
14+
return (
15+
<WalletSyncContext.Provider value={walletSyncState}>{children}</WalletSyncContext.Provider>
16+
);
17+
}

apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useDestroyTrustchain.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,32 @@ import {
66
memberCredentialsSelector,
77
} from "@ledgerhq/trustchain/store";
88
import { useMutation } from "@tanstack/react-query";
9-
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
109
import { setFlow } from "~/renderer/actions/walletSync";
1110
import { Flow, Step } from "~/renderer/reducers/walletSync";
1211
import { QueryKey } from "./type.hooks";
12+
import { useCloudSyncSDK } from "./useWatchWalletSync";
13+
import { walletSyncUpdate } from "@ledgerhq/live-wallet/store";
1314

1415
export function useDestroyTrustchain() {
1516
const dispatch = useDispatch();
1617
const sdk = useTrustchainSdk();
18+
const cloudSyncSDK = useCloudSyncSDK();
1719
const trustchain = useSelector(trustchainSelector);
1820
const memberCredentials = useSelector(memberCredentialsSelector);
1921

2022
const deleteMutation = useMutation({
21-
mutationFn: () =>
22-
sdk.destroyTrustchain(trustchain as Trustchain, memberCredentials as MemberCredentials),
23+
mutationFn: async () => {
24+
if (!trustchain || !memberCredentials) {
25+
return;
26+
}
27+
await cloudSyncSDK.destroy(trustchain, memberCredentials);
28+
await sdk.destroyTrustchain(trustchain, memberCredentials);
29+
},
2330
mutationKey: [QueryKey.destroyTrustchain, trustchain],
2431
onSuccess: () => {
2532
dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeleted }));
2633
dispatch(resetTrustchainStore());
34+
dispatch(walletSyncUpdate(null, 0));
2735
},
2836
onError: () => dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeletionError })),
2937
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useCallback } from "react";
2+
import { useDispatch } from "react-redux";
3+
import { MemberCredentials, Trustchain, TrustchainSDK } from "@ledgerhq/trustchain/types";
4+
import { setTrustchain, resetTrustchainStore } from "@ledgerhq/trustchain/store";
5+
import { TrustchainEjected } from "@ledgerhq/trustchain/errors";
6+
import { log } from "@ledgerhq/logs";
7+
8+
export function useOnTrustchainRefreshNeeded(
9+
trustchainSdk: TrustchainSDK,
10+
memberCredentials: MemberCredentials | null,
11+
): (trustchain: Trustchain) => Promise<void> {
12+
const dispatch = useDispatch();
13+
const onTrustchainRefreshNeeded = useCallback(
14+
async (trustchain: Trustchain) => {
15+
try {
16+
if (!memberCredentials) return;
17+
log("walletsync", "onTrustchainRefreshNeeded " + trustchain.rootId);
18+
const newTrustchain = await trustchainSdk.restoreTrustchain(trustchain, memberCredentials);
19+
setTrustchain(newTrustchain);
20+
} catch (e) {
21+
if (e instanceof TrustchainEjected) {
22+
dispatch(resetTrustchainStore());
23+
}
24+
}
25+
},
26+
[dispatch, trustchainSdk, memberCredentials],
27+
);
28+
return onTrustchainRefreshNeeded;
29+
}

apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { getEnv } from "@ledgerhq/live-env";
55
import { getSdk } from "@ledgerhq/trustchain/index";
66
import { withDevice } from "@ledgerhq/live-common/hw/deviceAccess";
77
import Transport from "@ledgerhq/hw-transport";
8+
import { trustchainLifecycle } from "@ledgerhq/live-wallet/walletsync/index";
9+
import { useStore } from "react-redux";
10+
import { walletSelector } from "~/renderer/reducers/wallet";
11+
import { walletSyncStateSelector } from "@ledgerhq/live-wallet/store";
812

913
export function runWithDevice<T>(
1014
deviceId: string | undefined,
@@ -28,7 +32,18 @@ export function useTrustchainSdk() {
2832
const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`;
2933
return { applicationId, name };
3034
}, []);
31-
const sdk = getSdk(isMockEnv, defaultContext);
35+
const store = useStore();
36+
const lifecycle = useMemo(
37+
() =>
38+
trustchainLifecycle({
39+
getCurrentWSState: () => walletSyncStateSelector(walletSelector(store.getState())),
40+
}),
41+
[store],
42+
);
43+
const sdk = useMemo(
44+
() => getSdk(isMockEnv, defaultContext, lifecycle),
45+
[isMockEnv, defaultContext, lifecycle],
46+
);
3247

3348
return sdk;
3449
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { useCallback, useEffect, useMemo, useState } from "react";
2+
import { useDispatch, useSelector, useStore } from "react-redux";
3+
import noop from "lodash/noop";
4+
import { CloudSyncSDK, UpdateEvent } from "@ledgerhq/live-wallet/cloudsync/index";
5+
import walletsync, {
6+
liveSlug,
7+
DistantState,
8+
walletSyncWatchLoop,
9+
LocalState,
10+
Schema,
11+
} from "@ledgerhq/live-wallet/walletsync/index";
12+
import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
13+
import { walletSelector } from "~/renderer/reducers/wallet";
14+
import { memberCredentialsSelector, trustchainSelector } from "@ledgerhq/trustchain/store";
15+
import { State } from "~/renderer/reducers";
16+
import { cache as bridgeCache } from "~/renderer/bridge/cache";
17+
import {
18+
setAccountNames,
19+
walletSyncStateSelector,
20+
walletSyncUpdate,
21+
} from "@ledgerhq/live-wallet/store";
22+
import { replaceAccounts } from "~/renderer/actions/accounts";
23+
import { latestDistantStateSelector } from "~/renderer/reducers/wallet";
24+
import { log } from "@ledgerhq/logs";
25+
import { useTrustchainSdk } from "./useTrustchainSdk";
26+
import { useOnTrustchainRefreshNeeded } from "./useOnTrustchainRefreshNeeded";
27+
import { Dispatch } from "redux";
28+
29+
function localStateSelector(state: State): LocalState {
30+
// READ. connect the redux state to the walletsync modules
31+
return {
32+
accounts: { list: state.accounts },
33+
accountNames: state.wallet.accountNames,
34+
};
35+
}
36+
37+
function saveUpdate(newLocalState: LocalState, dispatch: Dispatch) {
38+
// WRITE. save the state for the walletsync modules
39+
dispatch(setAccountNames(newLocalState.accountNames));
40+
dispatch(replaceAccounts(newLocalState.accounts.list)); // IMPORTANT: keep this one last, it's doing the DB:* trigger to save the data
41+
}
42+
43+
export function useCloudSyncSDK(): CloudSyncSDK<Schema> {
44+
const trustchainSdk = useTrustchainSdk();
45+
const store = useStore();
46+
const dispatch = useDispatch();
47+
const getCurrentVersion = useCallback(
48+
() => walletSyncStateSelector(walletSelector(store.getState())).version,
49+
[store],
50+
);
51+
52+
const saveNewUpdate = useCallback(
53+
async (event: UpdateEvent<DistantState>) => {
54+
log("walletsync", "saveNewUpdate", { event });
55+
switch (event.type) {
56+
case "new-data": {
57+
// we resolve incoming distant state changes
58+
const ctx = { getAccountBridge, bridgeCache, blacklistedTokenIds: [] };
59+
const state = store.getState();
60+
const latest = latestDistantStateSelector(state);
61+
const local = localStateSelector(state);
62+
const data = event.data;
63+
const resolved = await walletsync.resolveIncomingDistantState(ctx, local, latest, data);
64+
65+
if (resolved.hasChanges) {
66+
const version = event.version;
67+
const localState = localStateSelector(store.getState()); // fetch again latest state because it might have changed
68+
const newLocalState = walletsync.applyUpdate(localState, resolved.update); // we resolve in sync the new local state to save
69+
dispatch(walletSyncUpdate(data, version));
70+
saveUpdate(newLocalState, dispatch);
71+
log("walletsync", "resolved. changes applied.");
72+
} else {
73+
log("walletsync", "resolved. no changes to apply.");
74+
}
75+
break;
76+
}
77+
case "pushed-data": {
78+
dispatch(walletSyncUpdate(event.data, event.version));
79+
break;
80+
}
81+
case "deleted-data": {
82+
dispatch(walletSyncUpdate(null, 0));
83+
break;
84+
}
85+
}
86+
},
87+
[store, dispatch],
88+
);
89+
90+
const cloudSyncSDK = useMemo(
91+
() =>
92+
new CloudSyncSDK({
93+
slug: liveSlug,
94+
schema: walletsync.schema,
95+
trustchainSdk,
96+
getCurrentVersion,
97+
saveNewUpdate,
98+
}),
99+
[trustchainSdk, getCurrentVersion, saveNewUpdate],
100+
);
101+
102+
return cloudSyncSDK;
103+
}
104+
105+
export type WalletSyncUserState = {
106+
visualPending: boolean;
107+
walletSyncError: Error | null;
108+
onUserRefresh: () => void;
109+
};
110+
111+
export function useWatchWalletSync(): WalletSyncUserState {
112+
const store = useStore();
113+
const memberCredentials = useSelector(memberCredentialsSelector);
114+
const trustchain = useSelector(trustchainSelector);
115+
const trustchainSdk = useTrustchainSdk();
116+
const walletSyncSdk = useCloudSyncSDK();
117+
const onTrustchainRefreshNeeded = useOnTrustchainRefreshNeeded(trustchainSdk, memberCredentials);
118+
119+
const [visualPending, setVisualPending] = useState(true);
120+
const [walletSyncError, setWalletSyncError] = useState<Error | null>(null);
121+
const [onUserRefresh, setOnUserRefresh] = useState<() => void>(() => noop);
122+
const state = useMemo(
123+
() => ({ visualPending, walletSyncError, onUserRefresh }),
124+
[visualPending, walletSyncError, onUserRefresh],
125+
);
126+
127+
// pull and push wallet sync loop
128+
useEffect(() => {
129+
if (!trustchain || !memberCredentials) {
130+
setOnUserRefresh(() => noop);
131+
return;
132+
}
133+
134+
const { unsubscribe, onUserRefreshIntent } = walletSyncWatchLoop({
135+
walletSyncSdk,
136+
trustchain,
137+
memberCredentials,
138+
setVisualPending,
139+
getState: () => store.getState(),
140+
localStateSelector,
141+
latestDistantStateSelector,
142+
onError: e => setWalletSyncError(e && e instanceof Error ? e : new Error(String(e))),
143+
onStartPolling: () => setWalletSyncError(null),
144+
onTrustchainRefreshNeeded,
145+
});
146+
147+
setOnUserRefresh(() => onUserRefreshIntent);
148+
149+
return unsubscribe;
150+
}, [
151+
store,
152+
trustchainSdk,
153+
walletSyncSdk,
154+
trustchain,
155+
memberCredentials,
156+
onTrustchainRefreshNeeded,
157+
]);
158+
159+
return state;
160+
}

0 commit comments

Comments
 (0)