Skip to content

Commit

Permalink
feat: wallet sync synchronize via qr code from mobile to desktop
Browse files Browse the repository at this point in the history
  • Loading branch information
cgrellard-ledger committed Aug 22, 2024
1 parent efea3b0 commit 819d0e6
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 72 deletions.
8 changes: 8 additions & 0 deletions .changeset/dirty-drinks-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"ledger-live-desktop": minor
"live-mobile": minor
"@ledgerhq/trustchain": minor
"@ledgerhq/web-tools": minor
---

Ledger Sync - Added the synchronization of a trustchain from mobile to desktop by scanning the QR code
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getEnv } from "@ledgerhq/live-env";
import os from "os";

const platformMap: Record<string, string | undefined> = {
ios: "iOS",
android: "Android",
};

export function useInstanceName(): string {
const platform = os.platform();
const hash = getEnv("USER_ID").slice(0, 5);
const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`;
return name;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { useDispatch, useSelector } from "react-redux";
import { setFlow, setQrCodePinCode } from "~/renderer/actions/walletSync";
import { Flow, Step } from "~/renderer/reducers/walletSync";
Expand All @@ -10,6 +10,7 @@ import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useQueryClient } from "@tanstack/react-query";
import { QueryKey } from "./type.hooks";
import { useInstanceName } from "./useInstanceName";

export function useQRCode() {
const queryClient = useQueryClient();
Expand All @@ -24,13 +25,14 @@ export function useQRCode() {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
const memberName = useInstanceName();

const goToActivation = useCallback(() => {
dispatch(setFlow({ flow: Flow.Activation, step: Step.DeviceAction }));
}, [dispatch]);

const startQRCodeProcessing = useCallback(() => {
if (!trustchain || !memberCredentials) return;
if (!memberCredentials) return;

setError(null);
setIsLoading(true);
Expand All @@ -44,9 +46,15 @@ export function useQRCode() {
dispatch(setFlow({ flow: Flow.Synchronize, step: Step.PinCode }));
},
addMember: async member => {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
if (trustchain) {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
}
throw new NoTrustchainInitialized();
},
memberCredentials,
memberName,
alreadyHasATrustchain: !!trustchain,
})
.catch(e => {
if (e instanceof InvalidDigitsError) {
Expand All @@ -63,7 +71,7 @@ export function useQRCode() {
setIsLoading(false);
setError(null);
});
}, [trustchain, memberCredentials, trustchainApiBaseUrl, dispatch, sdk, queryClient]);
}, [memberCredentials, trustchainApiBaseUrl, memberName, dispatch, trustchain, sdk, queryClient]);

return {
url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { walletSyncStateSelector } from "@ledgerhq/live-wallet/store";
import { TrustchainSDK } from "@ledgerhq/trustchain/types";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useInstanceName } from "./useInstanceName";

const platformMap: Record<string, string | undefined> = {

Check failure on line 15 in apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts

View check run for this annotation

live-github-bot / @Desktop • Test App

@typescript-eslint/no-unused-vars

'platformMap' is assigned a value but never used.

Check failure on line 15 in apps/ledger-live-desktop/src/newArch/features/WalletSync/hooks/useTrustchainSdk.ts

View check run for this annotation

live-github-bot / @Desktop • Test App

@typescript-eslint/no-unused-vars

'platformMap' is assigned a value but never used.
darwin: "Mac",
Expand All @@ -24,14 +25,12 @@ export function useTrustchainSdk() {
const { trustchainApiBaseUrl, cloudSyncApiBaseUrl } = getWalletSyncEnvironmentParams(
featureWalletSync?.params?.environment,
);
const name = useInstanceName();
const isMockEnv = !!getEnv("MOCK");
const defaultContext = useMemo(() => {
const applicationId = 16;
const platform = os.platform();
const hash = getEnv("USER_ID").slice(0, 5);
const name = `${platformMap[platform] || platform}${hash ? " " + hash : ""}`;
return { applicationId, name, apiBaseUrl: trustchainApiBaseUrl };
}, [trustchainApiBaseUrl]);
}, [trustchainApiBaseUrl, name]);
const store = useStore();
const lifecycle = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React from "react";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import OpenOrInstallTrustChainApp from "../DeviceActions/openOrInstall";
import { useInitMemberCredentials } from "../../hooks/useInitMemberCredentials";

type Props = {
goNext: (device: Device) => void;
};

export default function DeviceActionStep({ goNext }: Props) {
useInitMemberCredentials();

return <OpenOrInstallTrustChainApp goNext={goNext} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SynchronizeWallet from "./Synchronize";
import WalletSyncManageInstances from "./ManageInstances";
import WalletSyncActivation from "./Activation";
import WalletSyncManage from "./Manage";
import { useInitMemberCredentials } from "../hooks/useInitMemberCredentials";

export interface BackRef {
goBack: () => void;
Expand All @@ -14,6 +15,7 @@ export interface BackRef {
export interface BackProps {}

export const WalletSyncRouter = forwardRef<BackRef, BackProps>((_props, ref) => {
useInitMemberCredentials();
const walletSyncFlow = useSelector(walletSyncFlowSelector);

switch (walletSyncFlow) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { useSelector } from "react-redux";
import { trustchainSelector, memberCredentialsSelector } from "@ledgerhq/trustchain/store";
import { useTrustchainSdk } from "./useTrustchainSdk";
Expand All @@ -11,6 +11,7 @@ import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useQueryClient } from "@tanstack/react-query";
import { QueryKey } from "./type.hooks";
import { useInstanceName } from "./useInstanceName";

interface Props {
setCurrentStep: (step: Steps) => void;
Expand All @@ -28,6 +29,7 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
const { trustchainApiBaseUrl } = getWalletSyncEnvironmentParams(
featureWalletSync?.params?.environment,
);
const memberName = useInstanceName();

const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState<string | null>(null);
Expand Down Expand Up @@ -55,9 +57,15 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
setCurrentStep(Steps.PinDisplay);
},
addMember: async member => {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
if (trustchain) {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
}
throw new NoTrustchainInitialized();
},
memberCredentials,
memberName,
alreadyHasATrustchain: !!trustchain,
})
.catch(e => {
if (e instanceof InvalidDigitsError) {
Expand Down Expand Up @@ -85,6 +93,7 @@ export function useQRCodeHost({ setCurrentStep, currentStep, currentOption }: Pr
memberCredentials,
isLoading,
trustchainApiBaseUrl,
memberName,
setCurrentStep,
sdk,
queryClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useCallback, useState } from "react";
import { MemberCredentials } from "@ledgerhq/trustchain/types";
import { MemberCredentials, TrustchainMember } from "@ledgerhq/trustchain/types";
import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index";
import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { setTrustchain } from "@ledgerhq/trustchain/store";
import { useDispatch } from "react-redux";
import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { setTrustchain, trustchainSelector } from "@ledgerhq/trustchain/store";
import { useDispatch, useSelector } from "react-redux";
import { useNavigation } from "@react-navigation/native";
import { Steps } from "../types/Activation";
import { NavigatorName, ScreenName } from "~/const";
import { useInstanceName } from "./useInstanceName";
import { useTrustchainSdk } from "./useTrustchainSdk";

export const useSyncWithQrCode = () => {
const [nbDigits, setDigits] = useState<number | null>(null);
const [input, setInput] = useState<string | null>(null);
const instanceName = useInstanceName();
const trustchain = useSelector(trustchainSelector);
const sdk = useTrustchainSdk();

const navigation = useNavigation();

Expand Down Expand Up @@ -46,13 +49,21 @@ export const useSyncWithQrCode = () => {
setCurrentStep: (step: Steps) => void,
) => {
try {
const trustchain = await createQRCodeCandidateInstance({
const newTrustchain = await createQRCodeCandidateInstance({
memberCredentials,
scannedUrl: url,
memberName: instanceName,
onRequestQRCodeInput,
addMember: async (member: TrustchainMember) => {
if (trustchain) {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
}
throw new NoTrustchainInitialized();
},
alreadyHasATrustchain: !!trustchain,
});
dispatch(setTrustchain(trustchain));
dispatch(setTrustchain(newTrustchain));
onSyncFinished();
return true;
} catch (e) {
Expand All @@ -66,7 +77,7 @@ export const useSyncWithQrCode = () => {
throw e;
}
},
[instanceName, onRequestQRCodeInput, onSyncFinished, dispatch],
[instanceName, onRequestQRCodeInput, trustchain, dispatch, onSyncFinished, sdk],
);

const handleSendDigits = useCallback(
Expand Down
1 change: 1 addition & 0 deletions apps/web-tools/trustchain/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ const App = () => {
<AppQRCodeCandidate
memberCredentials={memberCredentials}
setTrustchain={setTrustchain}
trustchain={trustchain}
/>
</Expand>
</Expand>
Expand Down
14 changes: 13 additions & 1 deletion apps/web-tools/trustchain/components/AppQRCodeCandidate.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import React, { useCallback, useState } from "react";
import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index";
import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
import { memberNameForPubKey } from "./IdentityManager";
import { Input } from "./Input";
import { useTrustchainSDK } from "../context";

export function AppQRCodeCandidate({
memberCredentials,
setTrustchain,
trustchain,
}: {
memberCredentials: MemberCredentials | null;
setTrustchain: (trustchain: Trustchain | null) => void;
trustchain: Trustchain | null;
}) {
const sdk = useTrustchainSDK();
const [scannedUrl, setScannedUrl] = useState<string | null>(null);
const [input, setInput] = useState<string | null>(null);
const [digits, setDigits] = useState<number | null>(null);
Expand All @@ -33,6 +37,14 @@ export function AppQRCodeCandidate({
scannedUrl,
memberName: memberNameForPubKey(memberCredentials.pubkey),
onRequestQRCodeInput,
addMember: async member => {
if (trustchain) {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
}
throw new NoTrustchainInitialized();
},
alreadyHasATrustchain: !!trustchain,
})
.then(trustchain => {
setTrustchain(trustchain);
Expand Down
12 changes: 9 additions & 3 deletions apps/web-tools/trustchain/components/AppQRCodeHost.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { InvalidDigitsError, NoTrustchainInitialized } from "@ledgerhq/trustchain/errors";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { RenderActionable } from "./Actionable";
import QRCode from "./QRCode";
Expand Down Expand Up @@ -30,9 +30,15 @@ export function AppQRCodeHost({
setDigits(digits);
},
addMember: async member => {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
if (trustchain) {
await sdk.addMember(trustchain, memberCredentials, member);
return trustchain;
}
throw new NoTrustchainInitialized();
},
memberCredentials,
memberName: "WebTools",
alreadyHasATrustchain: !!trustchain,
})
.catch(e => {
if (e instanceof InvalidDigitsError) {
Expand Down
4 changes: 4 additions & 0 deletions libs/trustchain/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ export const TrustchainEjected = createCustomErrorClass("TrustchainEjected");
export const TrustchainNotAllowed = createCustomErrorClass("TrustchainNotAllowed");
export const TrustchainOutdated = createCustomErrorClass("TrustchainOutdated");
export const TrustchainNotFound = createCustomErrorClass("TrustchainNotFound");
export const NoTrustchainInitialized = createCustomErrorClass("NoTrustchainInitialized");
export const TrustchainsAlreadyInitialized = createCustomErrorClass(
"TrustchainsAlreadyInitialized",
);
7 changes: 6 additions & 1 deletion libs/trustchain/src/qrcode/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe("Trustchain QR Code", () => {
};
const addMember = jest.fn(() => Promise.resolve(trustchain));
const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
const memberName = "foo";

let scannedUrlResolve: (url: string) => void;
const scannedUrlPromise = new Promise<string>(resolve => {
Expand All @@ -56,14 +57,18 @@ describe("Trustchain QR Code", () => {
onDisplayQRCode,
onDisplayDigits,
addMember,
memberCredentials,
memberName,
alreadyHasATrustchain: !!trustchain,
});

const scannedUrl = await scannedUrlPromise;
const memberName = "foo";

const candidateP = createQRCodeCandidateInstance({
memberCredentials,
memberName,
alreadyHasATrustchain: false,
addMember,
scannedUrl,
onRequestQRCodeInput,
});
Expand Down
Loading

0 comments on commit 819d0e6

Please sign in to comment.