diff --git a/apps/web-tools/package.json b/apps/web-tools/package.json
index 84f6fc56f3ad..b4ae277bba8d 100644
--- a/apps/web-tools/package.json
+++ b/apps/web-tools/package.json
@@ -29,11 +29,13 @@
"@ledgerhq/live-common": "workspace:^",
"@ledgerhq/live-config": "workspace:^",
"@ledgerhq/live-env": "workspace:^",
+ "@ledgerhq/live-wallet": "workspace:^",
"@ledgerhq/logs": "workspace:^",
"@ledgerhq/trustchain": "workspace:^",
"@ledgerhq/types-cryptoassets": "workspace:^",
"@ledgerhq/types-live": "workspace:^",
"bignumber.js": "^9.1.2",
+ "brace": "^0.11.1",
"bufferutil": "^4.0.7",
"encoding": "^0.1.13",
"eslint-config-next": "13.5.6",
diff --git a/apps/web-tools/trustchain/components/App.tsx b/apps/web-tools/trustchain/components/App.tsx
index 687e61b4c3f1..b23fa03413d9 100644
--- a/apps/web-tools/trustchain/components/App.tsx
+++ b/apps/web-tools/trustchain/components/App.tsx
@@ -8,7 +8,7 @@ import Expand from "./Expand";
import { getSdk } from "@ledgerhq/trustchain";
import { DisplayName, IdentityManager } from "./IdentityManager";
import { AppQRCodeCandidate } from "./AppQRCodeCandidate";
-import { SDKContext, defaultContext } from "../context";
+import { TrustchainSDKContext, defaultContext } from "../context";
import { AppQRCodeHost } from "./AppQRCodeHost";
import { AppMemberRow } from "./AppMemberRow";
import { AppDecryptUserData } from "./AppDecryptUserData";
@@ -22,6 +22,8 @@ import { AppInitLiveCredentials } from "./AppInitLiveCredentials";
import { AppMockEnv } from "./AppMockEnv";
import { AppSetTrustchainAPIEnv } from "./AppSetTrustchainAPIEnv";
import { AppRestoreTrustchain } from "./AppRestoreTrustchain";
+import { AppWalletSync } from "./AppWalletSync";
+import { AppSetCloudSyncAPIEnv } from "./AppSetCloudSyncAPIEnv";
const Container = styled.div`
padding: 0 10px;
@@ -64,9 +66,18 @@ const App = () => {
const mockEnv = useEnv("MOCK");
const sdk = useMemo(() => getSdk(!!mockEnv, context), [mockEnv, context]);
+ const envTrustchainApiIsStg = useEnv("TRUSTCHAIN_API").includes("stg");
+ const envWalletSyncApiIsStg = useEnv("WALLET_SYNC_API").includes("stg");
+ const envSummary = mockEnv
+ ? "MOCK"
+ : envTrustchainApiIsStg && envWalletSyncApiIsStg
+ ? "STG"
+ : !envTrustchainApiIsStg && !envWalletSyncApiIsStg
+ ? "PROD"
+ : "MIXED";
return (
-
+
Wallet Sync Trustchain Playground
@@ -93,12 +104,42 @@ const App = () => {
/>
-
+
+ Environment{" "}
+
+ {envSummary}
+
+ >
+ }
+ >
+
-
+
+ Trustchain SDK{" "}
+ {trustchain ? (
+
+ {trustchain.rootId.slice(0, 6)}..{trustchain.rootId.slice(-6)} at{" "}
+ {trustchain.applicationPath}
+
+ ) : null}
+ >
+ }
+ expanded={!trustchain}
+ >
{
setDeviceJWT={setDeviceJWT}
jwt={jwt}
/>
-
-
-
+
+
+
+
+
+
+
-
-
+
+ {trustchain && memberCredentials ? (
+
+ ) : (
+ "Please create a trustchain first"
+ )}
-
+
);
};
diff --git a/apps/web-tools/trustchain/components/AppAuthenticate.tsx b/apps/web-tools/trustchain/components/AppAuthenticate.tsx
index 46f2a460fccf..388694001981 100644
--- a/apps/web-tools/trustchain/components/AppAuthenticate.tsx
+++ b/apps/web-tools/trustchain/components/AppAuthenticate.tsx
@@ -1,8 +1,7 @@
import React, { useCallback } from "react";
-
import { JWT, MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppAuthenticate({
jwt,
@@ -17,7 +16,7 @@ export function AppAuthenticate({
trustchain: Trustchain | null;
deviceJWT: JWT | null;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(trustchain: Trustchain, memberCredentials: MemberCredentials) =>
diff --git a/apps/web-tools/trustchain/components/AppDecryptUserData.tsx b/apps/web-tools/trustchain/components/AppDecryptUserData.tsx
index 2bdc67f31111..f1da73bfb3ec 100644
--- a/apps/web-tools/trustchain/components/AppDecryptUserData.tsx
+++ b/apps/web-tools/trustchain/components/AppDecryptUserData.tsx
@@ -1,33 +1,24 @@
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { createQRCodeHostInstance } from "@ledgerhq/trustchain/qrcode/index";
+import React, { useCallback, useState } from "react";
import { crypto } from "@ledgerhq/hw-trustchain";
-import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
-import styled from "styled-components";
-import { Tooltip } from "react-tooltip";
-import { JWT, MemberCredentials, Trustchain, TrustchainMember } from "@ledgerhq/trustchain/types";
-import { getInitialStore } from "@ledgerhq/trustchain/store";
-import { Actionable, RenderActionable } from "./Actionable";
-import QRCode from "./QRCode";
-import useEnv from "../useEnv";
-import Expand from "./Expand";
-import { getSdk } from "@ledgerhq/trustchain";
-import { DisplayName, IdentityManager } from "./IdentityManager";
-import { AppQRCodeCandidate } from "./AppQRCodeCandidate";
+import { JWT, Trustchain } from "@ledgerhq/trustchain/types";
+import { Actionable } from "./Actionable";
import { Input } from "./Input";
-import { SDKContext, defaultContext, useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppDecryptUserData({ trustchain }: { trustchain: Trustchain | null }) {
const [input, setInput] = useState(null);
- const [output, setOutput] = useState<{ input: string } | null>(null);
- const sdk = useSDK();
+ const [output, setOutput] = useState(null);
+ const sdk = useTrustchainSDK();
const action = useCallback(
(trustchain: Trustchain, input: string) =>
- sdk.decryptUserData(trustchain, crypto.from_hex(input)).then(obj => obj as { input: string }),
+ sdk
+ .decryptUserData(trustchain, crypto.from_hex(input))
+ .then(array => new TextDecoder().decode(array)),
[sdk],
);
- const valueDisplay = useCallback((output: { input: string }) => {output.input}
, []);
+ const valueDisplay = useCallback((output: string) => {output}
, []);
return (
void;
setDeviceJWT: (deviceJWT: JWT | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(trustchain: Trustchain, jwt: JWT) =>
sdk.destroyTrustchain(trustchain, jwt).then(() => {
diff --git a/apps/web-tools/trustchain/components/AppDeviceAuthenticate.tsx b/apps/web-tools/trustchain/components/AppDeviceAuthenticate.tsx
index fdcfbbca2334..ff0aa7eec26a 100644
--- a/apps/web-tools/trustchain/components/AppDeviceAuthenticate.tsx
+++ b/apps/web-tools/trustchain/components/AppDeviceAuthenticate.tsx
@@ -1,6 +1,6 @@
import React, { useCallback } from "react";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
import { runWithDevice } from "../device";
export function AppDeviceAuthenticate({
@@ -10,7 +10,7 @@ export function AppDeviceAuthenticate({
deviceJWT: { accessToken: string } | null;
setDeviceJWT: (deviceJWT: { accessToken: string } | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
() => runWithDevice(transport => sdk.authWithDevice(transport)),
diff --git a/apps/web-tools/trustchain/components/AppEncryptUserData.tsx b/apps/web-tools/trustchain/components/AppEncryptUserData.tsx
index 637e4d896b2a..e93e3ffe0a3b 100644
--- a/apps/web-tools/trustchain/components/AppEncryptUserData.tsx
+++ b/apps/web-tools/trustchain/components/AppEncryptUserData.tsx
@@ -3,15 +3,16 @@ import { crypto } from "@ledgerhq/hw-trustchain";
import { Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
import { Input } from "./Input";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppEncryptUserData({ trustchain }: { trustchain: Trustchain | null }) {
const [input, setInput] = useState(null);
const [output, setOutput] = useState(null);
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
- (trustchain: Trustchain, input: string) => sdk.encryptUserData(trustchain, { input }),
+ (trustchain: Trustchain, input: string) =>
+ sdk.encryptUserData(trustchain, new TextEncoder().encode(input)),
[sdk],
);
diff --git a/apps/web-tools/trustchain/components/AppGetMembers.tsx b/apps/web-tools/trustchain/components/AppGetMembers.tsx
index 5765e2b4e956..2731a217df8f 100644
--- a/apps/web-tools/trustchain/components/AppGetMembers.tsx
+++ b/apps/web-tools/trustchain/components/AppGetMembers.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
import { JWT, Trustchain, TrustchainMember } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppGetMembers({
jwt,
@@ -14,7 +14,7 @@ export function AppGetMembers({
members: TrustchainMember[] | null;
setMembers: (members: TrustchainMember[] | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(jwt: JWT, trustchain: Trustchain) => sdk.getMembers(jwt, trustchain),
diff --git a/apps/web-tools/trustchain/components/AppGetOrCreateTrustchain.tsx b/apps/web-tools/trustchain/components/AppGetOrCreateTrustchain.tsx
index cef6d2284079..adf37d3c60eb 100644
--- a/apps/web-tools/trustchain/components/AppGetOrCreateTrustchain.tsx
+++ b/apps/web-tools/trustchain/components/AppGetOrCreateTrustchain.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
import { JWT, MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
import { runWithDevice } from "../device";
export function AppGetOrCreateTrustchain({
@@ -17,7 +17,7 @@ export function AppGetOrCreateTrustchain({
setTrustchain: (trustchain: Trustchain | null) => void;
setDeviceJWT: (deviceJWT: JWT | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(deviceJWT: JWT, memberCredentials: MemberCredentials) =>
diff --git a/apps/web-tools/trustchain/components/AppInitLiveCredentials.tsx b/apps/web-tools/trustchain/components/AppInitLiveCredentials.tsx
index ba0f968003f1..a4554d7fcf86 100644
--- a/apps/web-tools/trustchain/components/AppInitLiveCredentials.tsx
+++ b/apps/web-tools/trustchain/components/AppInitLiveCredentials.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
import { MemberCredentials } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppInitLiveCredentials({
memberCredentials,
@@ -10,7 +10,7 @@ export function AppInitLiveCredentials({
memberCredentials: MemberCredentials | null;
setMemberCredentials: (memberCredentials: MemberCredentials | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(() => sdk.initMemberCredentials(), [sdk]);
const valueDisplay = useCallback(
diff --git a/apps/web-tools/trustchain/components/AppMemberRow.tsx b/apps/web-tools/trustchain/components/AppMemberRow.tsx
index 17241db24b11..569b0e311a46 100644
--- a/apps/web-tools/trustchain/components/AppMemberRow.tsx
+++ b/apps/web-tools/trustchain/components/AppMemberRow.tsx
@@ -2,7 +2,7 @@ import React, { useCallback } from "react";
import { JWT, MemberCredentials, Trustchain, TrustchainMember } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
import { DisplayName } from "./IdentityManager";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
import { runWithDevice } from "../device";
export function AppMemberRow({
@@ -22,7 +22,7 @@ export function AppMemberRow({
setDeviceJWT: (deviceJWT: JWT | null) => void;
setMembers: (members: TrustchainMember[] | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(deviceJWT: JWT, trustchain: Trustchain, memberCredentials: MemberCredentials) =>
diff --git a/apps/web-tools/trustchain/components/AppQRCodeHost.tsx b/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
index 12fd3204ce83..389b2e1a8fad 100644
--- a/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
+++ b/apps/web-tools/trustchain/components/AppQRCodeHost.tsx
@@ -4,7 +4,7 @@ import { InvalidDigitsError } from "@ledgerhq/trustchain/errors";
import { MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { RenderActionable } from "./Actionable";
import QRCode from "./QRCode";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppQRCodeHost({
trustchain,
@@ -13,7 +13,7 @@ export function AppQRCodeHost({
trustchain: Trustchain | null;
memberCredentials: MemberCredentials | null;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const [error, setError] = useState(null);
const [url, setUrl] = useState(null);
const [digits, setDigits] = useState(null);
diff --git a/apps/web-tools/trustchain/components/AppRestoreTrustchain.tsx b/apps/web-tools/trustchain/components/AppRestoreTrustchain.tsx
index 67eb9a9e4694..d46526de2b29 100644
--- a/apps/web-tools/trustchain/components/AppRestoreTrustchain.tsx
+++ b/apps/web-tools/trustchain/components/AppRestoreTrustchain.tsx
@@ -1,7 +1,7 @@
import React, { useCallback } from "react";
import { JWT, MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
import { Actionable } from "./Actionable";
-import { useSDK } from "../context";
+import { useTrustchainSDK } from "../context";
export function AppRestoreTrustchain({
jwt,
@@ -14,7 +14,7 @@ export function AppRestoreTrustchain({
trustchain: Trustchain | null;
setTrustchain: (trustchain: Trustchain | null) => void;
}) {
- const sdk = useSDK();
+ const sdk = useTrustchainSDK();
const action = useCallback(
(jwt: JWT, trustchainId: string, memberCredentials: MemberCredentials) =>
sdk.restoreTrustchain(jwt, trustchainId, memberCredentials),
diff --git a/apps/web-tools/trustchain/components/AppSetCloudSyncAPIEnv.tsx b/apps/web-tools/trustchain/components/AppSetCloudSyncAPIEnv.tsx
new file mode 100644
index 000000000000..ad26fa9fe67a
--- /dev/null
+++ b/apps/web-tools/trustchain/components/AppSetCloudSyncAPIEnv.tsx
@@ -0,0 +1,23 @@
+import React, { useCallback, useState } from "react";
+import { setEnv, getEnvDefault } from "@ledgerhq/live-env";
+import { Actionable } from "./Actionable";
+import useEnv from "../useEnv";
+import { Input } from "./Input";
+
+export function AppSetCloudSyncAPIEnv() {
+ const env = useEnv("WALLET_SYNC_API");
+ const [localValue, setLocalValue] = useState(env);
+ const action = useCallback(() => Promise.resolve(localValue), [localValue]);
+ return (
+ setEnv("WALLET_SYNC_API", v || getEnvDefault("WALLET_SYNC_API"))}
+ valueDisplay={() => (
+ setLocalValue(e.target.value)} />
+ )}
+ />
+ );
+}
diff --git a/apps/web-tools/trustchain/components/AppWalletSync.tsx b/apps/web-tools/trustchain/components/AppWalletSync.tsx
new file mode 100644
index 000000000000..ee55715d2dc2
--- /dev/null
+++ b/apps/web-tools/trustchain/components/AppWalletSync.tsx
@@ -0,0 +1,146 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { JWT, MemberCredentials, Trustchain } from "@ledgerhq/trustchain/types";
+import { useTrustchainSDK } from "../context";
+import {
+ WalletSyncSDK,
+ AccountsData,
+ accountsSchema,
+ UpdateEvent,
+} from "@ledgerhq/live-wallet/walletsync/index";
+import { genAccount } from "@ledgerhq/coin-framework/mocks/account";
+import { getDefaultAccountName } from "@ledgerhq/live-wallet/accountName";
+import { Actionable } from "./Actionable";
+import { JsonEditor } from "./JsonEditor";
+
+export function AppWalletSync({
+ trustchain,
+ memberCredentials,
+}: {
+ trustchain: Trustchain;
+ memberCredentials: MemberCredentials;
+}) {
+ const trustchainSdk = useTrustchainSDK();
+
+ const [version, setVersion] = useState(0); // TODO this would need some persistance
+ const [data, setData] = useState(null);
+ const [json, setJson] = useState("");
+ const [error, setError] = useState(null);
+
+ const onJsonEditorChange = useCallback((value: string) => {
+ setJson(value);
+ try {
+ if (!value.trim()) {
+ setData(null);
+ setError(null);
+ return;
+ }
+ const data = JSON.parse(value);
+ const validated = accountsSchema.parse(data);
+ setData(validated);
+ setError(null);
+ } catch (e) {
+ setError("Invalid data: " + String(e));
+ }
+ }, []);
+
+ const versionRef = useRef(version);
+ useEffect(() => {
+ versionRef.current = version;
+ }, [version]);
+
+ const getCurrentVersion = useCallback(() => versionRef.current, []);
+
+ const saveNewUpdate = useCallback(async (event: UpdateEvent) => {
+ // in this current version, we just display the data as is, but in real app we would first reconciliate the account data and manage the sync
+ switch (event.type) {
+ case "new-data":
+ setVersion(event.version);
+ setData(event.data);
+ setJson(event.data ? JSON.stringify(event.data, null, 2) : "");
+ break;
+ case "pushed-data":
+ setVersion(event.version);
+ break;
+ case "deleted-data":
+ setVersion(0);
+ setJson("");
+ setData(null);
+ break;
+ }
+ }, []);
+
+ const walletSyncSdk = useMemo(() => {
+ return new WalletSyncSDK({
+ trustchainSdk,
+ getCurrentVersion,
+ saveNewUpdate,
+ });
+ }, [trustchainSdk, getCurrentVersion, saveNewUpdate]);
+
+ const onPull = useCallback(async () => {
+ const jwt = await trustchainSdk.auth(trustchain, memberCredentials);
+ await walletSyncSdk.pull(jwt, trustchain);
+ }, [trustchainSdk, trustchain, memberCredentials, walletSyncSdk]);
+
+ const onPush = useCallback(async () => {
+ if (!data) return;
+ const jwt = await trustchainSdk.auth(trustchain, memberCredentials);
+ await walletSyncSdk.push(jwt, trustchain, data);
+ }, [trustchainSdk, trustchain, memberCredentials, walletSyncSdk, data]);
+
+ const onGenRandomAccountData = useCallback(() => {
+ const names: Record = {};
+ const accounts = Array(Math.floor(5 * Math.random()))
+ .fill(0)
+ .map(() => {
+ const account = genAccount(Math.random().toString());
+ let name = getDefaultAccountName(account);
+ if (Math.random() > 0.5) {
+ name = "Renamed " + name;
+ }
+ names[account.id] = name;
+ return {
+ id: account.id,
+ currencyId: account.currency.id,
+ index: account.index,
+ seedIdentifier: account.seedIdentifier,
+ derivationMode: account.derivationMode,
+ freshAddress: account.freshAddress,
+ };
+ });
+ const data = { accounts, names };
+ // locally reset the editor
+ setData(data);
+ setJson(JSON.stringify(data, null, 2));
+ }, [setData]);
+
+ const onDestroy = useCallback(async () => {
+ const jwt = await trustchainSdk.auth(trustchain, memberCredentials);
+ await walletSyncSdk.destroy(jwt);
+ }, [trustchainSdk, trustchain, memberCredentials, walletSyncSdk]);
+
+ return (
+
+
+
Version: {version}
+
+
+
+
+
+
+ {error ?
{error}
: null}
+
+ );
+}
diff --git a/apps/web-tools/trustchain/components/JsonEditor.tsx b/apps/web-tools/trustchain/components/JsonEditor.tsx
new file mode 100644
index 000000000000..0f6c88a59156
--- /dev/null
+++ b/apps/web-tools/trustchain/components/JsonEditor.tsx
@@ -0,0 +1,46 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import type ace from "brace";
+
+export function JsonEditor({
+ value,
+ onChange,
+}: {
+ value: string;
+ onChange: (value: string) => void;
+}) {
+ const initialValueRef = useRef(value);
+ const editorRef = useRef(null);
+
+ // on value changes and it's diff from editor, we force it
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ if (editorRef.current && editorRef.current.getValue() !== value) {
+ console.log("FORCE", value);
+ editorRef.current.setValue(value);
+ }
+ }, 100);
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [value]);
+
+ useEffect(() => {
+ if (typeof window === "undefined") return;
+ const ace = require("brace");
+ require("brace/mode/json");
+ require("brace/theme/github");
+ const editor = ace.edit("editor");
+ editor.setTheme("ace/theme/github");
+ editor.getSession().setMode("ace/mode/json");
+ editor.setValue(initialValueRef.current);
+ editorRef.current = editor;
+ editor.on("change", () => {
+ onChange(editor.getValue());
+ });
+ return () => {
+ editor.destroy();
+ };
+ }, [onChange]);
+
+ return ;
+}
diff --git a/apps/web-tools/trustchain/context.ts b/apps/web-tools/trustchain/context.ts
index 839550d044de..e72915631094 100644
--- a/apps/web-tools/trustchain/context.ts
+++ b/apps/web-tools/trustchain/context.ts
@@ -4,6 +4,8 @@ import { getSdk } from "@ledgerhq/trustchain/lib-es/index";
export const defaultContext = { applicationId: 16, name: "WebTools" };
-export const SDKContext = React.createContext(getSdk(false, defaultContext));
+export const TrustchainSDKContext = React.createContext(
+ getSdk(false, defaultContext),
+);
-export const useSDK = () => useContext(SDKContext);
+export const useTrustchainSDK = () => useContext(TrustchainSDKContext);
diff --git a/libs/env/src/env.ts b/libs/env/src/env.ts
index caefdd0720c4..f1be4684874c 100644
--- a/libs/env/src/env.ts
+++ b/libs/env/src/env.ts
@@ -633,6 +633,11 @@ const envDefinitions = {
parser: boolParser,
desc: "is walletconnect enabled",
},
+ WALLET_SYNC_API: {
+ def: "https://cloud-sync-backend.api.aws.stg.ldg-tech.com",
+ parser: stringParser,
+ desc: "wallet sync api base url",
+ },
WITH_DEVICE_POLLING_DELAY: {
def: 500,
parser: floatParser,
diff --git a/libs/live-wallet/.unimportedrc.json b/libs/live-wallet/.unimportedrc.json
index b6afa383d8c0..a06b0f5ad390 100644
--- a/libs/live-wallet/.unimportedrc.json
+++ b/libs/live-wallet/.unimportedrc.json
@@ -1,5 +1,6 @@
{
"entry": [
+ "src/walletsync/index.ts",
"src/liveqr/cross.ts",
"src/liveqr/importAccounts.ts",
"src/ordering.ts",
diff --git a/libs/live-wallet/package.json b/libs/live-wallet/package.json
index 660dd5db062f..f552021a7a02 100644
--- a/libs/live-wallet/package.json
+++ b/libs/live-wallet/package.json
@@ -21,22 +21,27 @@
"types": "lib/index.d.ts",
"license": "Apache-2.0",
"dependencies": {
- "@ledgerhq/types-live": "workspace:*",
- "@ledgerhq/types-cryptoassets": "workspace:*",
"@ledgerhq/coin-framework": "workspace:*",
+ "@ledgerhq/compressjs": "github:LedgerHQ/compressjs#d9e8e4d994923e0ea76a32b97289bcccfe71b82e",
+ "@ledgerhq/cryptoassets": "workspace:*",
+ "@ledgerhq/devices": "workspace:*",
"@ledgerhq/live-env": "workspace:*",
+ "@ledgerhq/live-network": "workspace:*",
"@ledgerhq/live-promise": "workspace:*",
"@ledgerhq/logs": "workspace:*",
- "@ledgerhq/compressjs": "github:LedgerHQ/compressjs#d9e8e4d994923e0ea76a32b97289bcccfe71b82e",
- "@ledgerhq/devices": "workspace:*",
- "@ledgerhq/cryptoassets": "workspace:*",
- "rxjs": "7",
+ "@ledgerhq/trustchain": "workspace:*",
+ "@ledgerhq/types-cryptoassets": "workspace:*",
+ "@ledgerhq/types-live": "workspace:*",
+ "base64-js": "1",
+ "bignumber.js": "9",
+ "fflate": "^0.8.2",
"lodash": "4",
- "bignumber.js": "9"
+ "rxjs": "7",
+ "zod": "^3.22.4"
},
"devDependencies": {
- "@types/lodash": "4",
"@types/jest": "^29.5.10",
+ "@types/lodash": "4",
"jest": "^29.7.0",
"ts-jest": "^29.1.1"
},
diff --git a/libs/live-wallet/src/walletsync/api.ts b/libs/live-wallet/src/walletsync/api.ts
new file mode 100644
index 000000000000..a5ee9d76ce3b
--- /dev/null
+++ b/libs/live-wallet/src/walletsync/api.ts
@@ -0,0 +1,99 @@
+import z from "zod";
+import network from "@ledgerhq/live-network";
+import { getEnv } from "@ledgerhq/live-env";
+
+export type JWT = {
+ accessToken: string;
+};
+
+const schemaAtomicGetNoData = z.object({
+ status: z.literal("no-data"),
+});
+const schemaAtomicGetUpToDate = z.object({
+ status: z.literal("up-to-date"),
+});
+const schemaAtomicGetOutOfSync = z.object({
+ status: z.literal("out-of-sync"),
+ version: z.number(),
+ payload: z.string(),
+ date: z.string(),
+ info: z.string().optional(),
+});
+const schemaAtomicGetResponse = z.discriminatedUnion("status", [
+ schemaAtomicGetNoData,
+ schemaAtomicGetUpToDate,
+ schemaAtomicGetOutOfSync,
+]);
+export type APISyncResponse = z.infer;
+
+const schemaAtomicPostUpdated = z.object({
+ status: z.literal("updated"),
+});
+const schemaAtomicPostOutOfSync = z.object({
+ status: z.literal("out-of-sync"),
+ version: z.number(),
+ payload: z.string(),
+ date: z.string(),
+ info: z.string().optional(),
+});
+const schemaAtomicPostResponse = z.discriminatedUnion("status", [
+ schemaAtomicPostUpdated,
+ schemaAtomicPostOutOfSync,
+]);
+
+export type APISyncUpdateResponse = z.infer;
+
+// Fetch data status from cloud
+async function fetchDataStatus(
+ jwt: JWT,
+ datatype: string,
+ version?: number,
+): Promise {
+ const { data } = await network({
+ url: `${getEnv("WALLET_SYNC_API")}/atomic/v1/${datatype}`,
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${jwt.accessToken}`,
+ },
+ params: version !== undefined ? { version } : {},
+ });
+ return schemaAtomicGetResponse.parse(data);
+}
+
+// Upload new version of data to cloud
+async function uploadData(
+ jwt: JWT,
+ datatype: string,
+ version: number,
+ payload: string,
+): Promise {
+ const { data } = await network({
+ url: `${getEnv("WALLET_SYNC_API")}/atomic/v1/${datatype}?version=${version}`,
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${jwt.accessToken}`,
+ "Content-Type": "application/json",
+ },
+ data: {
+ payload,
+ },
+ });
+ return schemaAtomicPostResponse.parse(data);
+}
+
+// Delete data from cloud
+async function deleteData(jwt: JWT, datatype: string): Promise {
+ await network({
+ url: `${getEnv("WALLET_SYNC_API")}/atomic/v1/${datatype}`,
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${jwt.accessToken}`,
+ },
+ });
+}
+
+export default {
+ fetchDataStatus,
+ uploadData,
+ deleteData,
+};
diff --git a/libs/live-wallet/src/walletsync/datatypes/accounts.ts b/libs/live-wallet/src/walletsync/datatypes/accounts.ts
new file mode 100644
index 000000000000..d82918e99561
--- /dev/null
+++ b/libs/live-wallet/src/walletsync/datatypes/accounts.ts
@@ -0,0 +1,21 @@
+import { z } from "zod";
+
+export const accountDescriptorSchema = z.object({
+ id: z.string(),
+ currencyId: z.string(),
+ freshAddress: z.string(),
+ seedIdentifier: z.string(),
+ derivationMode: z.string(),
+ index: z.number(),
+});
+export type AccountDescriptor = z.infer;
+
+export const accountsDescriptorSchema = z.array(accountDescriptorSchema);
+
+export const schema = z.object({
+ accounts: accountsDescriptorSchema,
+ names: z.record(z.string()),
+ // NB: append more fields here when we have more needs in the future, but NEVER break a type
+});
+
+export type Data = z.infer;
diff --git a/libs/live-wallet/src/walletsync/index.ts b/libs/live-wallet/src/walletsync/index.ts
new file mode 100644
index 000000000000..c4c0f5219974
--- /dev/null
+++ b/libs/live-wallet/src/walletsync/index.ts
@@ -0,0 +1,3 @@
+export * from "./sdk";
+export { schema as accountsSchema } from "./datatypes/accounts";
+export type { Data as AccountsData } from "./datatypes/accounts";
diff --git a/libs/live-wallet/src/walletsync/sdk.ts b/libs/live-wallet/src/walletsync/sdk.ts
new file mode 100644
index 000000000000..21c14c0b565a
--- /dev/null
+++ b/libs/live-wallet/src/walletsync/sdk.ts
@@ -0,0 +1,112 @@
+import { Trustchain, TrustchainSDK } from "@ledgerhq/trustchain/types";
+import { Data, schema } from "./datatypes/accounts";
+import api, { JWT } from "./api";
+import Base64 from "base64-js";
+import { compress, decompress } from "fflate";
+
+export type UpdateEvent =
+ | {
+ type: "new-data";
+ data: Data;
+ version: number;
+ }
+ | {
+ type: "pushed-data";
+ version: number;
+ }
+ | {
+ type: "deleted-data";
+ };
+
+export class WalletSyncSDK {
+ trustchainSdk: TrustchainSDK;
+ getCurrentVersion: () => number | undefined;
+ saveNewUpdate: (updateEvent: UpdateEvent) => Promise;
+
+ constructor({
+ trustchainSdk,
+ getCurrentVersion,
+ saveNewUpdate,
+ }: {
+ trustchainSdk: TrustchainSDK;
+ /**
+ * returns the current version of the data, if available.
+ */
+ getCurrentVersion: () => number | undefined;
+ /**
+ * apply the data over the accounts and we also save the version.
+ */
+ saveNewUpdate: (event: UpdateEvent) => Promise;
+ }) {
+ this.trustchainSdk = trustchainSdk;
+ this.getCurrentVersion = getCurrentVersion;
+ this.saveNewUpdate = saveNewUpdate;
+ }
+
+ async push(jwt: JWT, trustchain: Trustchain, data: Data): Promise {
+ const validated = schema.parse(data);
+ const json = JSON.stringify(validated);
+ const bytes = new TextEncoder().encode(json);
+ const compressed = await new Promise((resolve, reject) =>
+ compress(bytes, (err, result) => (err ? reject(err) : resolve(result))),
+ );
+ const encrypted = await this.trustchainSdk.encryptUserData(trustchain, compressed);
+ const base64 = Base64.fromByteArray(encrypted);
+ const version = (this.getCurrentVersion() || 0) + 1;
+ const response = await api.uploadData(jwt, "accounts", version, base64);
+ switch (response.status) {
+ case "updated": {
+ await this.saveNewUpdate({
+ type: "pushed-data",
+ version,
+ });
+ break;
+ }
+ case "out-of-sync": {
+ // WHAT TO DO? maybe we ignore because in this case we just wait for a pull?
+ console.warn("out-of-sync", response);
+ }
+ }
+ }
+
+ async pull(jwt: JWT, trustchain: Trustchain): Promise {
+ const response = await api.fetchDataStatus(jwt, "accounts", this.getCurrentVersion());
+ switch (response.status) {
+ case "no-data": {
+ // no data, nothing to do
+ break;
+ }
+ case "up-to-date": {
+ // already up to date
+ break;
+ }
+ case "out-of-sync": {
+ const decrypted = await this.trustchainSdk
+ .decryptUserData(trustchain, Base64.toByteArray(response.payload))
+ .catch(e => {
+ // TODO if we fail to decrypt, it may mean we need to restore trustchain. and if it still fails and on specific error, we will have to eject. figure out how to integrate this in the pull lifecycle.
+ throw e;
+ });
+ const decompressed = await new Promise((resolve, reject) =>
+ decompress(decrypted, (err, result) => (err ? reject(err) : resolve(result))),
+ );
+ const json = JSON.parse(new TextDecoder().decode(decompressed));
+ const validated = schema.parse(json);
+ const version = response.version;
+ await this.saveNewUpdate({
+ type: "new-data",
+ data: validated,
+ version,
+ });
+ break;
+ }
+ }
+ }
+
+ async destroy(jwt: JWT): Promise {
+ await api.deleteData(jwt, "accounts");
+ await this.saveNewUpdate({
+ type: "deleted-data",
+ });
+ }
+}
diff --git a/libs/trustchain/src/mockSdk.ts b/libs/trustchain/src/mockSdk.ts
index 4f5dd8cafbde..576a6e7cd791 100644
--- a/libs/trustchain/src/mockSdk.ts
+++ b/libs/trustchain/src/mockSdk.ts
@@ -144,14 +144,14 @@ class MockSDK implements TrustchainSDK {
return Promise.resolve();
}
- encryptUserData(trustchain: Trustchain, obj: object): Promise {
+ encryptUserData(trustchain: Trustchain, input: Uint8Array): Promise {
assertTrustchain(trustchain);
- return Promise.resolve(new TextEncoder().encode(JSON.stringify(obj)));
+ return Promise.resolve(input);
}
- decryptUserData(trustchain: Trustchain, data: Uint8Array): Promise