@ledgerhq/ledger-key-ring-protocol 0.5.1-nightly.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.eslintrc.js +33 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.unimportedrc.json +16 -0
  4. package/CHANGELOG.md +299 -0
  5. package/LICENSE.txt +21 -0
  6. package/README.md +3 -0
  7. package/jest.config.js +13 -0
  8. package/lib/HWDeviceProvider.d.ts +25 -0
  9. package/lib/HWDeviceProvider.d.ts.map +1 -0
  10. package/lib/HWDeviceProvider.js +88 -0
  11. package/lib/HWDeviceProvider.js.map +1 -0
  12. package/lib/api.d.ts +77 -0
  13. package/lib/api.d.ts.map +1 -0
  14. package/lib/api.js +150 -0
  15. package/lib/api.js.map +1 -0
  16. package/lib/auth.d.ts +3 -0
  17. package/lib/auth.d.ts.map +1 -0
  18. package/lib/auth.js +79 -0
  19. package/lib/auth.js.map +1 -0
  20. package/lib/errors.d.ts +40 -0
  21. package/lib/errors.d.ts.map +1 -0
  22. package/lib/errors.js +18 -0
  23. package/lib/errors.js.map +1 -0
  24. package/lib/index.d.ts +6 -0
  25. package/lib/index.d.ts.map +1 -0
  26. package/lib/index.js +17 -0
  27. package/lib/index.js.map +1 -0
  28. package/lib/mockSdk.d.ts +22 -0
  29. package/lib/mockSdk.d.ts.map +1 -0
  30. package/lib/mockSdk.js +208 -0
  31. package/lib/mockSdk.js.map +1 -0
  32. package/lib/qrcode/cipher.d.ts +12 -0
  33. package/lib/qrcode/cipher.d.ts.map +1 -0
  34. package/lib/qrcode/cipher.js +69 -0
  35. package/lib/qrcode/cipher.js.map +1 -0
  36. package/lib/qrcode/cipher.test.d.ts +2 -0
  37. package/lib/qrcode/cipher.test.d.ts.map +1 -0
  38. package/lib/qrcode/cipher.test.js +40 -0
  39. package/lib/qrcode/cipher.test.js.map +1 -0
  40. package/lib/qrcode/index.d.ts +70 -0
  41. package/lib/qrcode/index.d.ts.map +1 -0
  42. package/lib/qrcode/index.js +312 -0
  43. package/lib/qrcode/index.js.map +1 -0
  44. package/lib/qrcode/index.test.d.ts +2 -0
  45. package/lib/qrcode/index.test.d.ts.map +1 -0
  46. package/lib/qrcode/index.test.js +131 -0
  47. package/lib/qrcode/index.test.js.map +1 -0
  48. package/lib/qrcode/types.d.ts +69 -0
  49. package/lib/qrcode/types.d.ts.map +1 -0
  50. package/lib/qrcode/types.js +3 -0
  51. package/lib/qrcode/types.js.map +1 -0
  52. package/lib/sdk.d.ts +31 -0
  53. package/lib/sdk.d.ts.map +1 -0
  54. package/lib/sdk.js +380 -0
  55. package/lib/sdk.js.map +1 -0
  56. package/lib/store.d.ts +71 -0
  57. package/lib/store.d.ts.map +1 -0
  58. package/lib/store.js +62 -0
  59. package/lib/store.js.map +1 -0
  60. package/lib/types.d.ts +181 -0
  61. package/lib/types.d.ts.map +1 -0
  62. package/lib/types.js +10 -0
  63. package/lib/types.js.map +1 -0
  64. package/lib-es/HWDeviceProvider.d.ts +25 -0
  65. package/lib-es/HWDeviceProvider.d.ts.map +1 -0
  66. package/lib-es/HWDeviceProvider.js +81 -0
  67. package/lib-es/HWDeviceProvider.js.map +1 -0
  68. package/lib-es/api.d.ts +77 -0
  69. package/lib-es/api.d.ts.map +1 -0
  70. package/lib-es/api.js +145 -0
  71. package/lib-es/api.js.map +1 -0
  72. package/lib-es/auth.d.ts +3 -0
  73. package/lib-es/auth.d.ts.map +1 -0
  74. package/lib-es/auth.js +75 -0
  75. package/lib-es/auth.js.map +1 -0
  76. package/lib-es/errors.d.ts +40 -0
  77. package/lib-es/errors.d.ts.map +1 -0
  78. package/lib-es/errors.js +15 -0
  79. package/lib-es/errors.js.map +1 -0
  80. package/lib-es/index.d.ts +6 -0
  81. package/lib-es/index.d.ts.map +1 -0
  82. package/lib-es/index.js +13 -0
  83. package/lib-es/index.js.map +1 -0
  84. package/lib-es/mockSdk.d.ts +22 -0
  85. package/lib-es/mockSdk.d.ts.map +1 -0
  86. package/lib-es/mockSdk.js +201 -0
  87. package/lib-es/mockSdk.js.map +1 -0
  88. package/lib-es/qrcode/cipher.d.ts +12 -0
  89. package/lib-es/qrcode/cipher.d.ts.map +1 -0
  90. package/lib-es/qrcode/cipher.js +61 -0
  91. package/lib-es/qrcode/cipher.js.map +1 -0
  92. package/lib-es/qrcode/cipher.test.d.ts +2 -0
  93. package/lib-es/qrcode/cipher.test.d.ts.map +1 -0
  94. package/lib-es/qrcode/cipher.test.js +38 -0
  95. package/lib-es/qrcode/cipher.test.js.map +1 -0
  96. package/lib-es/qrcode/index.d.ts +70 -0
  97. package/lib-es/qrcode/index.d.ts.map +1 -0
  98. package/lib-es/qrcode/index.js +304 -0
  99. package/lib-es/qrcode/index.js.map +1 -0
  100. package/lib-es/qrcode/index.test.d.ts +2 -0
  101. package/lib-es/qrcode/index.test.d.ts.map +1 -0
  102. package/lib-es/qrcode/index.test.js +126 -0
  103. package/lib-es/qrcode/index.test.js.map +1 -0
  104. package/lib-es/qrcode/types.d.ts +69 -0
  105. package/lib-es/qrcode/types.d.ts.map +1 -0
  106. package/lib-es/qrcode/types.js +2 -0
  107. package/lib-es/qrcode/types.js.map +1 -0
  108. package/lib-es/sdk.d.ts +31 -0
  109. package/lib-es/sdk.d.ts.map +1 -0
  110. package/lib-es/sdk.js +371 -0
  111. package/lib-es/sdk.js.map +1 -0
  112. package/lib-es/store.d.ts +71 -0
  113. package/lib-es/store.d.ts.map +1 -0
  114. package/lib-es/store.js +51 -0
  115. package/lib-es/store.js.map +1 -0
  116. package/lib-es/types.d.ts +181 -0
  117. package/lib-es/types.d.ts.map +1 -0
  118. package/lib-es/types.js +7 -0
  119. package/lib-es/types.js.map +1 -0
  120. package/mocks/scenarios/addSameMemberMultipleTimes.json +426 -0
  121. package/mocks/scenarios/create2trustchainInARow.json +616 -0
  122. package/mocks/scenarios/getOrCreateTransactionCases.json +591 -0
  123. package/mocks/scenarios/member3implicitlyAdded.json +648 -0
  124. package/mocks/scenarios/membersManySelfAdd.json +1427 -0
  125. package/mocks/scenarios/randomMemberTryToDestroy.json +371 -0
  126. package/mocks/scenarios/removeMemberWithTheWrongSeed.json +510 -0
  127. package/mocks/scenarios/removedMemberEjectedOnDeletedTrustchain.json +481 -0
  128. package/mocks/scenarios/removedMemberEjectedOnGetMembers.json +648 -0
  129. package/mocks/scenarios/removedMemberEjectedOnRestore.json +648 -0
  130. package/mocks/scenarios/removingAMemberCreatesAnInteraction.json +593 -0
  131. package/mocks/scenarios/removingYourselfIsForbidden.json +397 -0
  132. package/mocks/scenarios/success.json +978 -0
  133. package/mocks/scenarios/tokenExpires.json +371 -0
  134. package/mocks/scenarios/twoAddMembersFollowedByDeviceAdd.json +705 -0
  135. package/mocks/scenarios/userRefusesAuth.json +40 -0
  136. package/mocks/scenarios/userRefusesRemoveMember.json +542 -0
  137. package/package.json +91 -0
  138. package/scripts/README.md +15 -0
  139. package/scripts/e2e.ts +57 -0
  140. package/src/HWDeviceProvider.ts +105 -0
  141. package/src/__tests__/integration/mock.sdk.test.ts +47 -0
  142. package/src/__tests__/integration/sdk.test.ts +20 -0
  143. package/src/__tests__/tsconfig.json +8 -0
  144. package/src/__tests__/unit/sdk.test.ts +236 -0
  145. package/src/api.ts +202 -0
  146. package/src/auth.ts +81 -0
  147. package/src/errors.ts +18 -0
  148. package/src/index.ts +20 -0
  149. package/src/mockSdk.ts +253 -0
  150. package/src/qrcode/cipher.test.ts +30 -0
  151. package/src/qrcode/cipher.ts +63 -0
  152. package/src/qrcode/index.test.ts +138 -0
  153. package/src/qrcode/index.ts +395 -0
  154. package/src/qrcode/types.ts +70 -0
  155. package/src/sdk.ts +542 -0
  156. package/src/store.ts +99 -0
  157. package/src/types.ts +242 -0
  158. package/tests/scenarios/_template.ts +18 -0
  159. package/tests/scenarios/addSameMemberMultipleTimes.ts +20 -0
  160. package/tests/scenarios/create2trustchainInARow.ts +14 -0
  161. package/tests/scenarios/getOrCreateTransactionCases.ts +74 -0
  162. package/tests/scenarios/member3implicitlyAdded.ts +51 -0
  163. package/tests/scenarios/membersManySelfAdd.ts +18 -0
  164. package/tests/scenarios/randomMemberTryToDestroy.ts +23 -0
  165. package/tests/scenarios/removeMemberWithTheWrongSeed.ts +28 -0
  166. package/tests/scenarios/removedMemberEjectedOnDeletedTrustchain.ts +31 -0
  167. package/tests/scenarios/removedMemberEjectedOnGetMembers.ts +29 -0
  168. package/tests/scenarios/removedMemberEjectedOnRestore.ts +31 -0
  169. package/tests/scenarios/removingAMemberCreatesAnInteraction.ts +42 -0
  170. package/tests/scenarios/removingYourselfIsForbidden.ts +11 -0
  171. package/tests/scenarios/success.ts +94 -0
  172. package/tests/scenarios/tokenExpires.ts +20 -0
  173. package/tests/scenarios/twoAddMembersFollowedByDeviceAdd.ts +49 -0
  174. package/tests/scenarios/userRefusesAuth.ts +28 -0
  175. package/tests/scenarios/userRefusesRemoveMember.ts +66 -0
  176. package/tests/test-helpers/recordTrustchainSdkTests.ts +178 -0
  177. package/tests/test-helpers/replayTrustchainSdkTests.ts +141 -0
  178. package/tests/test-helpers/types.ts +45 -0
  179. package/tests/tsconfig.json +8 -0
  180. package/tsconfig.json +15 -0
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@ledgerhq/ledger-key-ring-protocol",
3
+ "version": "0.5.1-nightly.0",
4
+ "description": "Ledger Key Ring Protocol layer",
5
+ "keywords": [
6
+ "Ledger"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/LedgerHQ/ledger-live.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/LedgerHQ/ledger-live/issues"
14
+ },
15
+ "homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/ledger-key-ring-protocol",
16
+ "main": "lib/index.js",
17
+ "module": "lib-es/index.js",
18
+ "types": "lib/index.d.ts",
19
+ "typesVersions": {
20
+ "*": {
21
+ "*.json": [
22
+ "*.json"
23
+ ],
24
+ "*": [
25
+ "lib/*"
26
+ ],
27
+ "lib/*": [
28
+ "lib/*"
29
+ ],
30
+ "lib-es/*": [
31
+ "lib-es/*"
32
+ ]
33
+ }
34
+ },
35
+ "exports": {
36
+ "./lib/*": "./lib/*.js",
37
+ "./lib/*.js": "./lib/*.js",
38
+ "./lib-es/*": "./lib-es/*.js",
39
+ "./lib-es/*.js": "./lib-es/*.js",
40
+ "./*": {
41
+ "require": "./lib/*.js",
42
+ "default": "./lib-es/*.js"
43
+ },
44
+ "./*.js": {
45
+ "require": "./lib/*.js",
46
+ "default": "./lib-es/*.js"
47
+ },
48
+ ".": {
49
+ "require": "./lib/index.js",
50
+ "default": "./lib-es/index.js"
51
+ },
52
+ "./package.json": "./package.json"
53
+ },
54
+ "license": "Apache-2.0",
55
+ "dependencies": {
56
+ "base64-js": "1",
57
+ "isomorphic-ws": "5",
58
+ "rxjs": "^7.8.1",
59
+ "ws": "8",
60
+ "@ledgerhq/errors": "6.19.1",
61
+ "@ledgerhq/hw-transport": "6.31.4",
62
+ "@ledgerhq/hw-transport-mocker": "6.29.4",
63
+ "@ledgerhq/hw-ledger-key-ring-protocol": "0.2.1-nightly.0",
64
+ "@ledgerhq/live-env": "2.4.1-nightly.0",
65
+ "@ledgerhq/live-network": "2.0.3-nightly.0",
66
+ "@ledgerhq/logs": "6.12.0",
67
+ "@ledgerhq/speculos-transport": "0.1.8-nightly.0",
68
+ "@ledgerhq/types-devices": "^6.25.3"
69
+ },
70
+ "devDependencies": {
71
+ "@types/jest": "^29.5.10",
72
+ "bip39": "^3.0.4",
73
+ "expect": "^27.5.1",
74
+ "jest": "^29.7.0",
75
+ "msw": "^2.2.13",
76
+ "ts-jest": "^29.1.1",
77
+ "ts-node": "^10.9.2"
78
+ },
79
+ "scripts": {
80
+ "clean": "rimraf lib lib-es",
81
+ "build": "tsc && tsc -m ES6 --outDir lib-es",
82
+ "prewatch": "pnpm build",
83
+ "watch": "tsc --watch",
84
+ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache",
85
+ "lint:fix": "pnpm lint --fix",
86
+ "typecheck": "tsc --noEmit",
87
+ "unimported": "unimported",
88
+ "test": "jest",
89
+ "e2e": "ts-node scripts/e2e.ts"
90
+ }
91
+ }
@@ -0,0 +1,15 @@
1
+ ### Run end 2 end tests and generate missing integration tests
2
+
3
+ the e2e script will run all end 2 end tests and record the APDUs, network calls and crypto randomness in order to replay them deterministically in integration tests.
4
+
5
+ Just run the tests that miss snapshot mocks.
6
+
7
+ ```
8
+ pnpm trustchain e2e
9
+ ```
10
+
11
+ Run all the end2end tests regardless if there are snapshot generated.
12
+
13
+ ```
14
+ RUN_EVEN_IF_SNAPSHOT_EXISTS=1 pnpm trustchain e2e
15
+ ```
package/scripts/e2e.ts ADDED
@@ -0,0 +1,57 @@
1
+ /* eslint-disable no-console */
2
+ import fsPromises from "fs/promises";
3
+ import { listen } from "@ledgerhq/logs";
4
+ import { recordTestTrustchainSdk } from "../tests/test-helpers/recordTrustchainSdkTests";
5
+ import path from "path";
6
+ import expect from "expect";
7
+
8
+ // we force inject expect to allow us to have expect() code in the test-scenarios
9
+ (global as any).expect = expect;
10
+
11
+ const coinapps = process.env.COIN_APPS;
12
+ if (!coinapps) {
13
+ throw new Error(
14
+ "COIN_APPS env is required. it should be the path to the coinapps folder ( cloned from https://github.com/LedgerHQ/coin-apps )",
15
+ );
16
+ }
17
+
18
+ async function main() {
19
+ const scenarioFolder = path.join(__dirname, "../tests/scenarios");
20
+ const mockFolder = path.join(__dirname, "../mocks/scenarios");
21
+ const files = await fsPromises.readdir(scenarioFolder);
22
+
23
+ for (const file of files) {
24
+ if (file.endsWith(".ts") && !file.startsWith("_")) {
25
+ const slug = file.slice(0, -3);
26
+ const e2eFile = path.join(scenarioFolder, file);
27
+ const mod = await import(e2eFile);
28
+ const scenario = mod.scenario;
29
+ const snapshotFile = path.join(mockFolder, slug + ".json");
30
+ const snapshotFileExists = await exists(snapshotFile);
31
+ if (snapshotFileExists && !process.env.RUN_EVEN_IF_SNAPSHOT_EXISTS) {
32
+ continue;
33
+ }
34
+ console.log("RUNNING E2E ON TEST SCENARIO", slug);
35
+ // otherwise we always run the e2e but don't touch existing snapshots
36
+ await recordTestTrustchainSdk(snapshotFileExists ? null : snapshotFile, scenario, {
37
+ coinapps,
38
+ ...mod.recorderConfig,
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ main();
45
+
46
+ function exists(file: string): Promise<boolean> {
47
+ return fsPromises.access(file).then(
48
+ () => true,
49
+ () => false,
50
+ );
51
+ }
52
+
53
+ if (process.env.VERBOSE) {
54
+ listen(log => {
55
+ console.log(log.type + ": " + log.message);
56
+ });
57
+ }
@@ -0,0 +1,105 @@
1
+ import { from, lastValueFrom } from "rxjs";
2
+ import { UserRefusedOnDevice } from "@ledgerhq/errors";
3
+ import { ApduDevice } from "@ledgerhq/hw-ledger-key-ring-protocol/ApduDevice";
4
+ import { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";
5
+ import { crypto, device } from "@ledgerhq/hw-ledger-key-ring-protocol";
6
+ import getApi from "./api";
7
+ import { genericWithJWT } from "./auth";
8
+ import { AuthCachePolicy, JWT, TrustchainDeviceCallbacks, WithDevice } from "./types";
9
+ import { TrustchainNotAllowed } from "./errors";
10
+
11
+ export class HWDeviceProvider {
12
+ /**
13
+ * TODO withDevice should be imported statically from @ledgerhq/live-common/hw/deviceAccess
14
+ *
15
+ * but ATM making @ledgerhq/live-common a dependency of @ledgerhq/ledger-key-ring-protocol causes:
16
+ * > Turbo error: Invalid package dependency graph: cyclic dependency detected:
17
+ * > @ledgerhq/ledger-key-ring-protocol,@ledgerhq/live-wallet,@ledgerhq/live-common
18
+ *
19
+ * Maybe hw/deviceAccess.ts and hw/index.ts could be moved to @ledgerhq/devices
20
+ * This would break the cyclic dependency as @ledgerhq/live-common would depend on @ledgerhq/devices
21
+ * but not the other way around.
22
+ */
23
+ private withDevice: WithDevice;
24
+ private jwt?: JWT;
25
+ private api: ReturnType<typeof getApi>;
26
+
27
+ constructor(apiBaseURL: string, withDevice: WithDevice) {
28
+ this.api = getApi(apiBaseURL);
29
+ this.withDevice = withDevice;
30
+ }
31
+
32
+ public withJwt<T>(
33
+ deviceId: string,
34
+ job: (jwt: JWT) => Promise<T>,
35
+ policy?: AuthCachePolicy,
36
+ callbacks?: TrustchainDeviceCallbacks,
37
+ ): Promise<T> {
38
+ return genericWithJWT(
39
+ jwt => {
40
+ this.jwt = jwt;
41
+ return job(jwt);
42
+ },
43
+ this.jwt,
44
+ () => this._authWithDevice(deviceId, callbacks),
45
+ (jwt: JWT) => this.api.refreshAuth(jwt),
46
+ policy,
47
+ );
48
+ }
49
+
50
+ public async withHw<T>(
51
+ deviceId: string,
52
+ job: (hw: ApduDevice) => Promise<T>,
53
+ callbacks?: TrustchainDeviceCallbacks,
54
+ ): Promise<T> {
55
+ callbacks?.onStartRequestUserInteraction?.();
56
+ const runWithDevice = this.withDevice(deviceId);
57
+ try {
58
+ return await lastValueFrom(runWithDevice(transport => from(job(device.apdu(transport)))));
59
+ } catch (error) {
60
+ if (!(error instanceof TransportStatusError)) {
61
+ throw error;
62
+ }
63
+ switch (error.statusCode) {
64
+ case StatusCodes.USER_REFUSED_ON_DEVICE:
65
+ case StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
66
+ throw new UserRefusedOnDevice();
67
+
68
+ case StatusCodes.TRUSTCHAIN_WRONG_SEED:
69
+ this.clearJwt();
70
+ throw new TrustchainNotAllowed();
71
+
72
+ default:
73
+ throw error;
74
+ }
75
+ } finally {
76
+ callbacks?.onEndRequestUserInteraction?.();
77
+ }
78
+ }
79
+
80
+ public async refreshJwt(deviceId: string, callbacks?: TrustchainDeviceCallbacks): Promise<void> {
81
+ this.jwt = await this.withJwt(deviceId, this.api.refreshAuth, "cache", callbacks);
82
+ }
83
+
84
+ public clearJwt() {
85
+ this.jwt = undefined;
86
+ }
87
+
88
+ private async _authWithDevice(
89
+ deviceId: string,
90
+ callbacks?: TrustchainDeviceCallbacks,
91
+ ): Promise<JWT> {
92
+ const challenge = await this.api.getAuthenticationChallenge();
93
+ const data = crypto.from_hex(challenge.tlv);
94
+ const seedId = await this.withHw(deviceId, hw => hw.getSeedId(data), callbacks);
95
+ const signature = crypto.to_hex(seedId.signature);
96
+ return this.api.postChallengeResponse({
97
+ challenge: challenge.json,
98
+ signature: {
99
+ credential: seedId.pubkeyCredential.toJSON(),
100
+ signature,
101
+ attestation: crypto.to_hex(seedId.attestationResult),
102
+ },
103
+ });
104
+ }
105
+ }
@@ -0,0 +1,47 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer";
4
+ import { RecordStore } from "@ledgerhq/hw-transport-mocker";
5
+ import { getEnv, setEnv } from "@ledgerhq/live-env";
6
+ import { ScenarioOptions } from "../../../tests/test-helpers/types";
7
+ import { getSdk } from "../..";
8
+ import { WithDevice } from "../../types";
9
+
10
+ setEnv("MOCK", "true");
11
+
12
+ const nonMockableScenarios = [
13
+ "randomMemberTryToDestroy", // can't simulate seed<>trustchain relationship
14
+ "removeMemberWithTheWrongSeed", // can't simulate seed<>trustchain relationship
15
+ "tokenExpires", // can't simulate token expiration
16
+ "userRefusesAuth", // can't simulate device interaction at the moment
17
+ "userRefusesRemoveMember", // can't simulate device interaction at the moment
18
+ ];
19
+
20
+ const scenarioFolder = path.join(__dirname, "../../../tests/scenarios");
21
+ fs.readdirSync(scenarioFolder).forEach(file => {
22
+ if (file.endsWith(".ts") && !file.startsWith("_")) {
23
+ const slug = file.slice(0, -3);
24
+ if (nonMockableScenarios.includes(slug)) return;
25
+ const e2eFile = path.join(scenarioFolder, file);
26
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
27
+ const mod = require(e2eFile);
28
+ test(slug, async () => {
29
+ const scenario = mod.scenario;
30
+ const transport = new TransportReplayer(new RecordStore());
31
+ const device = { id: "", transport };
32
+ const withDevice: WithDevice = () => fn => fn(device.transport);
33
+ const options: ScenarioOptions = {
34
+ withDevice,
35
+ sdkForName: name =>
36
+ getSdk(
37
+ !!getEnv("MOCK"),
38
+ { applicationId: 16, name, apiBaseUrl: getEnv("TRUSTCHAIN_API_STAGING") },
39
+ withDevice,
40
+ ),
41
+ pauseRecorder: () => Promise.resolve(), // replayer don't need to pause
42
+ switchDeviceSeed: async () => device, // nothing to actually do, we will continue replaying
43
+ };
44
+ await scenario(device.id, options);
45
+ });
46
+ }
47
+ });
@@ -0,0 +1,20 @@
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { replayTrustchainSdkTests } from "../../../tests/test-helpers/replayTrustchainSdkTests";
5
+
6
+ const mockFolder = path.join(__dirname, "../../../mocks/scenarios");
7
+ const scenarioFolder = path.join(__dirname, "../../../tests/scenarios");
8
+
9
+ fs.readdirSync(mockFolder).forEach(file => {
10
+ if (!file.endsWith(".json")) return;
11
+
12
+ const slug = file.slice(0, -5);
13
+ const json = require(path.join(mockFolder, file));
14
+ const mod = require(path.join(scenarioFolder, slug + ".ts"));
15
+
16
+ test(slug, async () => {
17
+ const scenario = mod.scenario;
18
+ await replayTrustchainSdkTests(json, scenario);
19
+ });
20
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "esModuleInterop": true,
5
+ "lib": ["es2020"],
6
+ "types": ["jest"]
7
+ }
8
+ }
@@ -0,0 +1,236 @@
1
+ import { DefaultBodyType, http, HttpResponse, PathParams, StrictRequest } from "msw";
2
+ import { setupServer } from "msw/node";
3
+ import {
4
+ CommandBlock,
5
+ crypto,
6
+ Device,
7
+ Permissions,
8
+ SoftwareDevice,
9
+ StreamTree,
10
+ } from "@ledgerhq/hw-ledger-key-ring-protocol";
11
+ import { getEnv } from "@ledgerhq/live-env";
12
+ import { PutCommandsRequest } from "../../api";
13
+ import { HWDeviceProvider } from "../../HWDeviceProvider";
14
+ import { convertLiveCredentialsToKeyPair, SDK } from "../../sdk";
15
+ import { TrustchainResultType } from "../../types";
16
+
17
+ describe("Trustchain SDK", () => {
18
+ // Setup API calls mocks
19
+ const apiMocks = {
20
+ getTrustchainsMock: jest.fn(),
21
+ getTrustchainByIdMock: jest.fn(),
22
+ getChalenge: jest.fn(),
23
+ postAuthenticate: jest.fn(),
24
+ postSeed: jest.fn<object, [StrictRequest<CommandBlock[]>]>(),
25
+ postDerivation: jest.fn<object, [StrictRequest<CommandBlock[]>]>(),
26
+ putCommands: jest.fn<object, [StrictRequest<PutCommandsRequest>]>(),
27
+ };
28
+ const mswServer = setupServer(
29
+ http.get("*/v1/trustchains", () => HttpResponse.json(apiMocks.getTrustchainsMock())),
30
+ http.get("*/v1/trustchain/ROOTID", () => HttpResponse.json(apiMocks.getTrustchainByIdMock())),
31
+ http.get("*/v1/challenge", () => HttpResponse.json(apiMocks.getChalenge())),
32
+ http.post("*/v1/authenticate", () => HttpResponse.json(apiMocks.postAuthenticate())),
33
+ http.post<PathParams, CommandBlock[]>("*/v1/seed", ({ request }) =>
34
+ HttpResponse.json(apiMocks.postSeed(request)),
35
+ ),
36
+ http.post<PathParams, CommandBlock[]>("*/v1/trustchain/ROOTID/derivation", ({ request }) =>
37
+ HttpResponse.json(apiMocks.postDerivation(request)),
38
+ ),
39
+ http.put<PathParams, PutCommandsRequest>("*/v1/trustchain/ROOTID/commands", ({ request }) =>
40
+ HttpResponse.json(apiMocks.putCommands(request)),
41
+ ),
42
+ http.all("*", () => HttpResponse.json({})),
43
+ );
44
+ mswServer.listen();
45
+
46
+ // Setup APDU device interactions mocks
47
+ const HWDeviceProviderMethodsMocks = {
48
+ withJwt: jest.fn(),
49
+ withHw: jest.fn(),
50
+ refreshJwt: jest.fn(),
51
+ clearJwt: jest.fn(),
52
+ } satisfies Partial<HWDeviceProvider>;
53
+ const hwDeviceProviderMock = HWDeviceProviderMethodsMocks as unknown as HWDeviceProvider;
54
+
55
+ afterAll(() => {
56
+ mswServer.close();
57
+ });
58
+
59
+ const apiBaseUrl = getEnv("TRUSTCHAIN_API_STAGING");
60
+ const sdkContext = { applicationId: 16, name: "alice", apiBaseUrl };
61
+
62
+ beforeEach(() => {
63
+ Object.values(apiMocks).forEach(mock => mock.mockClear());
64
+ Object.values(HWDeviceProviderMethodsMocks).forEach(mock => mock.mockClear());
65
+ });
66
+
67
+ it("encryptUserData + decryptUserData", async () => {
68
+ const sdk = new SDK(sdkContext, hwDeviceProviderMock);
69
+ const obj = new Uint8Array([1, 2, 3, 4, 5]);
70
+ const keypair = await crypto.randomKeypair();
71
+ const trustchain = {
72
+ rootId: "",
73
+ walletSyncEncryptionKey: crypto.to_hex(keypair.privateKey),
74
+ applicationPath: "m/0'/16'/0'",
75
+ };
76
+ const encrypted = await sdk.encryptUserData(trustchain, obj);
77
+ const decrypted = await sdk.decryptUserData(trustchain, encrypted);
78
+ expect(decrypted).toEqual(obj);
79
+ });
80
+
81
+ it("should create Trustchain", async () => {
82
+ const { alice } = MOCK_DATA.members;
83
+
84
+ // Mock trustchain states:
85
+ const device = new SoftwareDevice(convertLiveCredentialsToKeyPair(alice));
86
+ const initialTree = await createTrustChain(device);
87
+ const oneMemberTree = await addMember(device, "m/0'/16'/0'", "alice")(initialTree);
88
+
89
+ // Mock API calls:
90
+ apiMocks.getTrustchainsMock.mockReturnValueOnce({});
91
+ apiMocks.getTrustchainsMock.mockReturnValueOnce({ ROOTID: initialTree.serialize() });
92
+ apiMocks.getTrustchainByIdMock.mockReturnValue(initialTree.serialize());
93
+
94
+ // Mock APDU device interactions:
95
+ HWDeviceProviderMethodsMocks.withJwt.mockImplementation(async (deviceId, job) =>
96
+ job({ accessToken: "ACCESS TOKEN" }),
97
+ );
98
+ HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(initialTree);
99
+ HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(oneMemberTree);
100
+
101
+ // Run the test:
102
+ const sdk = new SDK(sdkContext, hwDeviceProviderMock);
103
+ const { type, trustchain } = await sdk.getOrCreateTrustchain("", alice);
104
+
105
+ // Check expectations:
106
+ expect(type).toBe(TrustchainResultType.created);
107
+ expect(trustchain).toEqual({
108
+ applicationPath: "m/0'/16'/0'",
109
+ rootId: "ROOTID",
110
+ walletSyncEncryptionKey: expect.stringMatching(/^[0-9a-f]{64}$/),
111
+ });
112
+
113
+ expect(await jsonRequestContent(apiMocks.postSeed)).toEqual([oneMemberTree.serialize()["m/"]]);
114
+ expect(await jsonRequestContent(apiMocks.postDerivation)).toEqual([
115
+ oneMemberTree.serialize()["m/0'/16'/0'"],
116
+ ]);
117
+ expect(apiMocks.putCommands).not.toHaveBeenCalled();
118
+
119
+ expect(HWDeviceProviderMethodsMocks.withJwt).toHaveBeenCalled();
120
+ expect(HWDeviceProviderMethodsMocks.withHw).toHaveBeenCalledTimes(2);
121
+ });
122
+
123
+ it("should remove a member from the Trustchain", async () => {
124
+ const { alice, bob, charlie } = MOCK_DATA.members;
125
+
126
+ // Mock trustchain states:
127
+ const device = new SoftwareDevice(convertLiveCredentialsToKeyPair(alice));
128
+ const closedStreamTree = await createTrustChain(device)
129
+ .then(addMember(device, "m/0'/16'/0'", "alice"))
130
+ .then(addMember(device, "m/0'/16'/0'", "bob"))
131
+ .then(addMember(device, "m/0'/16'/0'", "charlie"))
132
+ .then(tree => tree.close("m/0'/16'/0'", device));
133
+ const rmMembersTree = await addMember(device, "m/0'/16'/1'", "alice")(closedStreamTree);
134
+
135
+ // Mock API calls:
136
+ apiMocks.getTrustchainByIdMock.mockReturnValue(closedStreamTree.serialize());
137
+ apiMocks.getChalenge.mockReturnValue({ json: {}, tlv: MOCK_DATA.challengeTlv });
138
+ apiMocks.postAuthenticate.mockReturnValue({
139
+ accessToken: "BACKEND JWT",
140
+ permissions: { ROOTID: { "m/0'/16'/1'": ["owner"] } },
141
+ });
142
+
143
+ // Mock APDU device interactions:
144
+ HWDeviceProviderMethodsMocks.withJwt.mockImplementation(async (deviceId, job) =>
145
+ job({ accessToken: "ACCESS TOKEN" }),
146
+ );
147
+ HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(closedStreamTree);
148
+ HWDeviceProviderMethodsMocks.withHw.mockResolvedValueOnce(rmMembersTree);
149
+
150
+ // Mock the lifecycle callback:
151
+ const afterRotation = jest.fn();
152
+ const onTrustchainRotation = jest.fn();
153
+ onTrustchainRotation.mockResolvedValueOnce(afterRotation);
154
+
155
+ // Run the test:
156
+ const sdk = new SDK(sdkContext, hwDeviceProviderMock, { onTrustchainRotation });
157
+
158
+ const trustchain = {
159
+ applicationPath: "m/0'/16'/0'",
160
+ rootId: "ROOTID",
161
+ walletSyncEncryptionKey: "",
162
+ };
163
+ const memberToRemove = {
164
+ name: "bob",
165
+ id: bob.pubkey,
166
+ permissions: Permissions.OWNER,
167
+ };
168
+ const newTrustchain = await sdk.removeMember("", trustchain, alice, memberToRemove);
169
+
170
+ // Check expectations:
171
+ expect(newTrustchain).toEqual({
172
+ applicationPath: "m/0'/16'/1'",
173
+ rootId: "ROOTID",
174
+ walletSyncEncryptionKey: expect.stringMatching(/^[0-9a-f]{64}$/),
175
+ });
176
+
177
+ expect(await jsonRequestContent(apiMocks.postDerivation)).toEqual([
178
+ rmMembersTree.serialize()["m/0'/16'/1'"],
179
+ ]);
180
+
181
+ const putCommands = await jsonRequestContent(apiMocks.putCommands);
182
+ const closeBlock = putCommands.find(_ => _.path === "m/0'/16'/0'")?.blocks[0] ?? "";
183
+ const pushMemberBlock = putCommands.find(_ => _.path === "m/0'/16'/1'")?.blocks[0] ?? "";
184
+ expect(putCommands).toEqual([
185
+ { path: "m/0'/16'/1'", blocks: [pushMemberBlock] },
186
+ { path: "m/0'/16'/0'", blocks: [closeBlock] }, // The closed stream command is sent last
187
+ ]);
188
+ expect(closeBlock).toBe(closedStreamTree.serialize()["m/0'/16'/0'"].slice(-closeBlock.length));
189
+ expect(pushMemberBlock).not.toContain(bob.pubkey);
190
+ expect(pushMemberBlock).toContain(charlie.pubkey);
191
+
192
+ expect(HWDeviceProviderMethodsMocks.withJwt).toHaveBeenCalled();
193
+ expect(HWDeviceProviderMethodsMocks.withHw).toHaveBeenCalledTimes(2);
194
+ expect(HWDeviceProviderMethodsMocks.refreshJwt).toHaveBeenCalledTimes(1);
195
+
196
+ expect(onTrustchainRotation).toHaveBeenCalledWith(sdk, trustchain, alice);
197
+ expect(afterRotation).toHaveBeenCalledWith(newTrustchain);
198
+ });
199
+ });
200
+
201
+ const MOCK_DATA = {
202
+ members: {
203
+ alice: {
204
+ pubkey: "02e3311a12c450604725f02d1a775ef5cdb4a1b832eb41ac6b1302adbe92a612fc",
205
+ privatekey: "873f500bd20783224f7e78d4f8cce3d2bf69eb8008fbd697d20bbea31a721a03",
206
+ },
207
+ bob: {
208
+ pubkey: "034ac6813695b0d5e033a2a19061c83951e2241aad62ec8fa347a944831c07ea82",
209
+ },
210
+ charlie: {
211
+ pubkey: "03b4165cddf39e58f3a89682fdf1ccba213167084fecdbb3b86669d40d201df1cf",
212
+ },
213
+ },
214
+
215
+ challengeTlv:
216
+ "010107020100121053801a35c2e24b627d6e4925ce318980140101154630440220319b42a416512437e48d9c9bf204daea7da03d452c50a8caa4c2d152407ffd0c02201f121b0e99df1d30f4757b6a00b8d974d70996771893ac49c4a245c147cc1d8f160466a90248202b7472757374636861696e2d6261636b656e642e6170692e6177732e7374672e6c64672d746563682e636f6d320121332103cb7628e7248ddf9c07da54b979f16bf081fb3d173aac0992ad2a44ef6a388ae2600401000000",
217
+ };
218
+
219
+ function createTrustChain(device: Device): Promise<StreamTree> {
220
+ return StreamTree.createNewTree(device);
221
+ }
222
+
223
+ function addMember(
224
+ device: Device,
225
+ path: string,
226
+ name: keyof typeof MOCK_DATA.members,
227
+ ): (tree: StreamTree) => Promise<StreamTree> {
228
+ const memberId = crypto.from_hex(MOCK_DATA.members[name].pubkey);
229
+ return (tree: StreamTree) => tree.share(path, device, memberId, name, Permissions.OWNER);
230
+ }
231
+
232
+ function jsonRequestContent<T extends DefaultBodyType>(
233
+ mock: jest.Mock<object, [StrictRequest<T>]>,
234
+ ): Promise<T[]> {
235
+ return Promise.all(mock.mock.calls.map(([request]) => request.json()));
236
+ }