Skip to content

Commit

Permalink
LLD integration of Wallet Sync (#7300)
Browse files Browse the repository at this point in the history
chore: kickoff LLD integration of wallet sync
  • Loading branch information
gre committed Jul 18, 2024
1 parent 98297e5 commit d026c19
Show file tree
Hide file tree
Showing 24 changed files with 467 additions and 182 deletions.
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/src/main/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ async function reload() {
const encryptedDataPaths = [
["app", "accounts"],
["app", "trustchain"],
["app", "wallet"],
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";
import { useWatchWalletSync, WalletSyncUserState } from "../hooks/useWatchWalletSync";

export const WalletSyncContext = React.createContext<WalletSyncUserState>({
visualPending: false,
walletSyncError: null,
onUserRefresh: () => {},
});

export const useWalletSyncUserState = () => React.useContext(WalletSyncContext);

export function WalletSyncProvider({ children }: { children: React.ReactNode }) {
const walletSyncState = useWatchWalletSync();
return (
<WalletSyncContext.Provider value={walletSyncState}>{children}</WalletSyncContext.Provider>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
deviceId: string | undefined,
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Schema> {
const trustchainSdk = useTrustchainSdk();
const store = useStore();
const dispatch = useDispatch();
const getCurrentVersion = useCallback(
() => walletSyncStateSelector(walletSelector(store.getState())).version,
[store],
);

const saveNewUpdate = useCallback(
async (event: UpdateEvent<DistantState>) => {
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<Error | null>(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;
}
Loading

0 comments on commit d026c19

Please sign in to comment.