Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LLD integration of Wallet Sync #7300

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading