@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
@@ -0,0 +1,94 @@
1
+ import { ScenarioOptions } from "../test-helpers/types";
2
+
3
+ /**
4
+ * a complete scenario with 3 members and various sdk successful interactions.
5
+ */
6
+ export async function scenario(deviceId: string, { sdkForName }: ScenarioOptions) {
7
+ // first member initializes itself
8
+ const name1 = "Member 1";
9
+ const sdk1 = sdkForName(name1);
10
+ const member1creds = await sdk1.initMemberCredentials();
11
+
12
+ // auth with the device and init the first trustchain
13
+ const { trustchain } = await sdk1.getOrCreateTrustchain(deviceId, member1creds);
14
+
15
+ // verify we have member 1 in the trustchain
16
+ const members = await sdk1.getMembers(trustchain, member1creds);
17
+ expect(members).toEqual([
18
+ {
19
+ id: member1creds.pubkey,
20
+ name: name1,
21
+ permissions: 0xffffffff,
22
+ },
23
+ ]);
24
+
25
+ // second member initializes itself
26
+ const name2 = "Member 2";
27
+ const sdk2 = sdkForName(name2);
28
+ const member2creds = await sdk2.initMemberCredentials();
29
+
30
+ // member 1 adds member 2 (= qr code flow)
31
+ await sdk1.addMember(trustchain, member1creds, {
32
+ name: name2,
33
+ id: member2creds.pubkey,
34
+ permissions: 0xffffffff,
35
+ });
36
+
37
+ // member2 list members and verify it's correct
38
+ const members2 = await sdk2.getMembers(trustchain, member2creds);
39
+ const expectedMembers = [
40
+ {
41
+ id: member1creds.pubkey,
42
+ name: name1,
43
+ permissions: 0xffffffff,
44
+ },
45
+ {
46
+ id: member2creds.pubkey,
47
+ name: name2,
48
+ permissions: 0xffffffff,
49
+ },
50
+ ];
51
+ expect(members2).toEqual(expectedMembers);
52
+
53
+ // third member initializes itself
54
+ const name3 = "Member 3";
55
+ const sdk3 = sdkForName(name3);
56
+ const member3creds = await sdk3.initMemberCredentials();
57
+
58
+ // member 2 adds member 3
59
+ await sdk2.addMember(trustchain, member2creds, {
60
+ name: name3,
61
+ id: member3creds.pubkey,
62
+ permissions: 0xffffffff,
63
+ });
64
+ expectedMembers.push({
65
+ id: member3creds.pubkey,
66
+ name: name3,
67
+ permissions: 0xffffffff,
68
+ });
69
+
70
+ // member1 can also list members and see third member
71
+ const members1 = await sdk1.getMembers(trustchain, member1creds);
72
+ expect(members1).toEqual(expectedMembers);
73
+
74
+ // as well as member3 itself
75
+ expect(await sdk3.getMembers(trustchain, member3creds)).toEqual(expectedMembers);
76
+
77
+ // member1 removes member2
78
+ const newTrustchain = await sdk1.removeMember(deviceId, trustchain, member1creds, members2[1]);
79
+ expectedMembers.splice(1, 1);
80
+
81
+ // verify the trustchain has rotated
82
+ expect(newTrustchain.walletSyncEncryptionKey).not.toBe(trustchain.walletSyncEncryptionKey);
83
+ expect(newTrustchain.applicationPath).not.toBe(trustchain.applicationPath);
84
+
85
+ // member1 can still list members
86
+ expect(await sdk1.getMembers(newTrustchain, member1creds)).toEqual(expectedMembers);
87
+
88
+ // member3 that may not have refreshed yet, can now restore the trustchain
89
+ const restored = await sdk3.restoreTrustchain(trustchain, member3creds);
90
+ expect(restored).toEqual(newTrustchain);
91
+
92
+ // member2 destroy the trustchain
93
+ await sdk2.destroyTrustchain(trustchain, member2creds);
94
+ }
@@ -0,0 +1,20 @@
1
+ import { ScenarioOptions } from "../test-helpers/types";
2
+ import { HWDeviceProvider } from "../../src/HWDeviceProvider";
3
+ import { SDK } from "../../src/sdk";
4
+ import { getEnv } from "@ledgerhq/live-env";
5
+
6
+ export async function scenario(deviceId: string, { withDevice, pauseRecorder }: ScenarioOptions) {
7
+ const apiBaseUrl = getEnv("TRUSTCHAIN_API_STAGING");
8
+ const hwDeviceProvider = new HWDeviceProvider(apiBaseUrl, withDevice);
9
+ const applicationId = 16;
10
+ const sdk = new SDK({ applicationId, name: "Foo", apiBaseUrl }, hwDeviceProvider);
11
+ const creds = await sdk.initMemberCredentials();
12
+
13
+ const jwt1 = await hwDeviceProvider.withJwt(deviceId, jwt => Promise.resolve(jwt));
14
+ await pauseRecorder(6 * 60 * 1000);
15
+ const { trustchain } = await sdk.getOrCreateTrustchain(deviceId, creds);
16
+ const jwt2 = await hwDeviceProvider.withJwt(deviceId, jwt2 => Promise.resolve(jwt2));
17
+ // assert that jwt was refreshed (due to the expiration)
18
+ expect(jwt1).not.toEqual(jwt2);
19
+ await sdk.destroyTrustchain(trustchain, creds);
20
+ }
@@ -0,0 +1,49 @@
1
+ import { ScenarioOptions } from "../test-helpers/types";
2
+
3
+ /**
4
+ * a complete scenario with 3 members and various sdk successful interactions.
5
+ */
6
+ export async function scenario(deviceId: string, { sdkForName }: ScenarioOptions) {
7
+ // members
8
+ const name1 = "Member 1";
9
+ const sdk1 = sdkForName(name1);
10
+ const member1creds = await sdk1.initMemberCredentials();
11
+
12
+ const name2 = "Member 2";
13
+ const sdk2 = sdkForName(name2);
14
+ const member2creds = await sdk2.initMemberCredentials();
15
+ const member2 = { name: name2, id: member2creds.pubkey, permissions: 0xffffffff };
16
+
17
+ const name3 = "Member 3";
18
+ const sdk3 = sdkForName(name3);
19
+ const member3creds = await sdk3.initMemberCredentials();
20
+ const member3 = { name: name3, id: member3creds.pubkey, permissions: 0xffffffff };
21
+
22
+ const name4 = "Member 4";
23
+ const sdk4 = sdkForName(name4);
24
+ const member4creds = await sdk4.initMemberCredentials();
25
+
26
+ // auth with the device and init the first trustchain
27
+ const { trustchain } = await sdk1.getOrCreateTrustchain(deviceId, member1creds);
28
+
29
+ // member 1 adds member 2
30
+ await sdk1.addMember(trustchain, member1creds, member2);
31
+
32
+ // member 1 adds member 3
33
+ await sdk1.addMember(trustchain, member1creds, member3);
34
+
35
+ // member 4 implicits add itself with device auth
36
+ const { trustchain: trustchain4 } = await sdk4.getOrCreateTrustchain(deviceId, member4creds);
37
+ expect(trustchain).toEqual(trustchain4);
38
+
39
+ // list members
40
+ const members = await sdk3.getMembers(trustchain, member3creds);
41
+ expect(members.map(m => m.id)).toEqual([
42
+ member1creds.pubkey,
43
+ member2creds.pubkey,
44
+ member3creds.pubkey,
45
+ member4creds.pubkey,
46
+ ]);
47
+
48
+ await sdk2.destroyTrustchain(trustchain, member2creds);
49
+ }
@@ -0,0 +1,28 @@
1
+ import { UserRefusedOnDevice } from "@ledgerhq/errors";
2
+ import { RecorderConfig, ScenarioOptions, recorderConfigDefaults } from "../test-helpers/types";
3
+
4
+ export async function scenario(deviceId: string, { sdkForName }: ScenarioOptions) {
5
+ const sdk1 = sdkForName("Foo");
6
+ const memberCredentials = await sdk1.initMemberCredentials();
7
+ let interactionCounter = 0;
8
+ let totalInteractionCounter = 0;
9
+ const callbacks = {
10
+ onStartRequestUserInteraction: () => {
11
+ totalInteractionCounter++;
12
+ interactionCounter++;
13
+ },
14
+ onEndRequestUserInteraction: () => {
15
+ interactionCounter--;
16
+ },
17
+ };
18
+ await expect(sdk1.getOrCreateTrustchain(deviceId, memberCredentials, callbacks)).rejects.toThrow(
19
+ UserRefusedOnDevice,
20
+ );
21
+ expect(interactionCounter).toBe(0);
22
+ expect(totalInteractionCounter).toBe(1);
23
+ }
24
+
25
+ export const recorderConfig: RecorderConfig = {
26
+ goNextOnText: recorderConfigDefaults.goNextOnText.concat(["Log in to"]),
27
+ approveOnText: ["Cancel login"],
28
+ };
@@ -0,0 +1,66 @@
1
+ import { UserRefusedOnDevice } from "@ledgerhq/errors";
2
+ import { RecorderConfig, ScenarioOptions } from "../test-helpers/types";
3
+
4
+ export async function scenario(deviceId: string, { sdkForName }: ScenarioOptions) {
5
+ // first member initializes itself
6
+ const name1 = "Member 1";
7
+ const sdk1 = sdkForName(name1);
8
+ const member1creds = await sdk1.initMemberCredentials();
9
+ const member1 = { id: member1creds.pubkey, name: name1, permissions: 0xffffffff };
10
+
11
+ // auth with the device and init the first trustchain
12
+ const { trustchain } = await sdk1.getOrCreateTrustchain(deviceId, member1creds);
13
+
14
+ // second member initializes itself
15
+ const name2 = "Member 2";
16
+ const sdk2 = sdkForName(name2);
17
+ const member2creds = await sdk2.initMemberCredentials();
18
+ const member2 = { id: member2creds.pubkey, name: name2, permissions: 0xffffffff };
19
+
20
+ // member1 adds member2 (= qr code flow)
21
+ await sdk1.addMember(trustchain, member1creds, member2);
22
+
23
+ // list members and verify it's correct
24
+ expect(await sdk2.getMembers(trustchain, member2creds)).toEqual([member1, member2]);
25
+
26
+ // member1 refuses to remove member2
27
+ let interactionCounter = 0;
28
+ let totalInteractionCounter = 0;
29
+ const callbacks = {
30
+ onStartRequestUserInteraction: () => {
31
+ totalInteractionCounter++;
32
+ interactionCounter++;
33
+ },
34
+ onEndRequestUserInteraction: () => {
35
+ interactionCounter--;
36
+ },
37
+ };
38
+ await expect(
39
+ sdk1.removeMember(deviceId, trustchain, member1creds, member2, callbacks),
40
+ ).rejects.toThrow(UserRefusedOnDevice);
41
+ expect(interactionCounter).toBe(0);
42
+ expect(totalInteractionCounter).toBe(3);
43
+
44
+ // make sure the member2 is still there
45
+ expect(await sdk2.getMembers(trustchain, member2creds)).toEqual([member1, member2]);
46
+ }
47
+
48
+ export const recorderConfig: RecorderConfig = {
49
+ approveOnceOnText: ["Enable", "Confirm"], // Approve the first interaction (After login)
50
+ approveOnText: ["Log in to", "Don't enable"],
51
+
52
+ goNextOnText: [
53
+ // Login:
54
+ ...[
55
+ "Login request",
56
+ "Identify with your",
57
+ "request",
58
+ "Ensure you trust the",
59
+ "update request",
60
+ "keep",
61
+ ],
62
+
63
+ // Refuse the second interaction (remove member):
64
+ "Enable",
65
+ ],
66
+ };
@@ -0,0 +1,178 @@
1
+ import fsPromises from "fs/promises";
2
+ import { setupServer } from "msw/node";
3
+ import { RecordStore } from "@ledgerhq/hw-transport-mocker";
4
+ import { createSpeculosDevice, releaseSpeculosDevice } from "@ledgerhq/speculos-transport";
5
+ import { DeviceModelId } from "@ledgerhq/types-devices";
6
+ import { crypto, TRUSTCHAIN_APP_NAME } from "@ledgerhq/hw-ledger-key-ring-protocol";
7
+ import { getEnv, setEnv } from "@ledgerhq/live-env";
8
+ import { RecorderConfig, ScenarioOptions, genSeed, recorderConfigDefaults } from "./types";
9
+ import { getSdk } from "../../src";
10
+ import { WithDevice } from "../../src/types";
11
+
12
+ setEnv("GET_CALLS_RETRY", 0);
13
+
14
+ export async function recordTestTrustchainSdk(
15
+ file: string | null,
16
+ scenario: (deviceId: string, scenarioOptions: ScenarioOptions) => Promise<void>,
17
+ config: RecorderConfig,
18
+ ) {
19
+ const seed = config.seed || genSeed();
20
+ const coinapps = config.coinapps;
21
+ if (!coinapps) throw new Error("coinapps is required"); // it's completed by e2e script
22
+
23
+ const goNextOnText = config.goNextOnText || recorderConfigDefaults.goNextOnText;
24
+ const approveOnceOnText = config.approveOnceOnText || [];
25
+ const approveOnText = config.approveOnText || recorderConfigDefaults.approveOnText;
26
+
27
+ const buttonClicksPromises: Array<Promise<void>> = [];
28
+ const recordStore = new RecordStore();
29
+
30
+ const createDeviceWithSeed = async (seed: string) => {
31
+ const device = await createSpeculosDevice({
32
+ model: DeviceModelId.nanoSP,
33
+ firmware: "1.1.2",
34
+ appName: TRUSTCHAIN_APP_NAME,
35
+ appVersion: "1.0.1",
36
+ seed,
37
+ coinapps, // folder where there is the Ledger Sync coin app
38
+ });
39
+
40
+ // passthrough all success cases for the Ledger Sync coin app to accept all.
41
+ const sub = device.transport.automationEvents.subscribe(event => {
42
+ const approveOnceIndex = approveOnceOnText.findIndex(t => event.text.trim() == t);
43
+ if (approveOnceIndex > -1) {
44
+ approveOnceOnText.splice(approveOnceIndex, 1);
45
+ buttonClicksPromises.push(device.transport.button("both"));
46
+ } else if (goNextOnText.some(t => event.text.trim() == t)) {
47
+ buttonClicksPromises.push(device.transport.button("right"));
48
+ } else if (approveOnText.some(t => event.text.trim() == t)) {
49
+ buttonClicksPromises.push(device.transport.button("both"));
50
+ }
51
+ });
52
+
53
+ // monkey patch the transport to record all device APDU exchanges
54
+ const transport = device.transport;
55
+ const originalExchange = transport.exchange;
56
+ transport.exchange = async function (apdu: Buffer) {
57
+ const out = await originalExchange.call(transport, apdu);
58
+ recordStore.recordExchange(apdu, out);
59
+ return out;
60
+ };
61
+
62
+ return { device, sub };
63
+ };
64
+
65
+ // listen to network with msw to be able to replay in our future tests
66
+ const transactions: Transaction[] = [];
67
+ const server = setupServer();
68
+ server.events.on("response:bypass", ({ response, request }) => {
69
+ if (request.url.startsWith("http://localhost")) return; // ignore speculos requests
70
+ const transaction: Transaction = {
71
+ request: {
72
+ url: request.url,
73
+ method: request.method,
74
+ headers: headersToJson(request.headers),
75
+ },
76
+ response: {
77
+ status: response.status,
78
+ headers: headersToJson(response.headers),
79
+ },
80
+ };
81
+ transactions.push(transaction);
82
+ if (request.body) {
83
+ request.text().then(body => {
84
+ transaction.request.body = body;
85
+ });
86
+ }
87
+ if (response.body) {
88
+ response.text().then(body => {
89
+ transaction.response.body = body;
90
+ });
91
+ }
92
+ });
93
+
94
+ // Monkey patches the `crypto.randomBytes` method to log generated random bytes in hexadecimal format in order to deterministically replay them in unit tests.
95
+ const randomBytesOutputs: string[] = [];
96
+ const originalRandomBytes = crypto.randomBytes;
97
+ crypto.randomBytes = async (size: number) => {
98
+ const bytes = await originalRandomBytes.call(crypto, size);
99
+ randomBytesOutputs.push(crypto.to_hex(bytes));
100
+ return bytes;
101
+ };
102
+
103
+ // Monkey patches the `crypto.randomKeypair` method to log generated random keypairs in hexadecimal format in order to deterministically replay them in unit tests.
104
+ const randomKeypairOutputs: string[] = [];
105
+ const originalRandomKeypair = crypto.randomKeypair;
106
+ crypto.randomKeypair = async () => {
107
+ const keypair = await originalRandomKeypair.call(crypto);
108
+ randomKeypairOutputs.push(crypto.to_hex(keypair.privateKey));
109
+ return keypair;
110
+ };
111
+
112
+ let { device, sub } = await createDeviceWithSeed(seed);
113
+ const withDevice: WithDevice = () => fn => fn(device.transport);
114
+ const options: ScenarioOptions = {
115
+ withDevice,
116
+ sdkForName: name =>
117
+ getSdk(
118
+ !!getEnv("MOCK"),
119
+ { applicationId: 16, name, apiBaseUrl: getEnv("TRUSTCHAIN_API_STAGING") },
120
+ withDevice,
121
+ ),
122
+ pauseRecorder: async (milliseconds: number) => {
123
+ await new Promise(resolve => setTimeout(resolve, milliseconds));
124
+ },
125
+ switchDeviceSeed: async (newSeed?: string) => {
126
+ // release and replace previous device
127
+ await releaseSpeculosDevice(device.id);
128
+ const res = await createDeviceWithSeed(newSeed || genSeed());
129
+ device = res.device;
130
+ sub = res.sub;
131
+ return device;
132
+ },
133
+ };
134
+
135
+ // Run the scenario with speculos simulator and with all networking recorded.
136
+ server.listen({ onUnhandledRequest: "bypass" });
137
+ try {
138
+ await scenario(device.id, options);
139
+ } finally {
140
+ sub.unsubscribe();
141
+ await Promise.all(buttonClicksPromises);
142
+ await releaseSpeculosDevice(device.id);
143
+ }
144
+ server.close();
145
+
146
+ if (file) {
147
+ const json = {
148
+ apdus: recordStore.toString(),
149
+ crypto: { randomBytesOutputs, randomKeypairOutputs },
150
+ http: { transactions },
151
+ };
152
+
153
+ // Write the transactions to the disk.
154
+ await fsPromises.writeFile(file, JSON.stringify(json, null, 2));
155
+ }
156
+ }
157
+
158
+ function headersToJson(headers) {
159
+ const obj: Record<string, string> = {};
160
+ for (const [key, value] of headers.entries()) {
161
+ obj[key] = value;
162
+ }
163
+ return obj;
164
+ }
165
+
166
+ type Transaction = {
167
+ request: {
168
+ url: string;
169
+ method: string;
170
+ headers: Record<string, string>;
171
+ body?: string;
172
+ };
173
+ response: {
174
+ status: number;
175
+ headers: Record<string, string>;
176
+ body?: string;
177
+ };
178
+ };
@@ -0,0 +1,141 @@
1
+ import { http, HttpResponse } from "msw";
2
+ import { setupServer } from "msw/node";
3
+ import { crypto } from "@ledgerhq/hw-ledger-key-ring-protocol";
4
+ import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker";
5
+ import { getEnv, setEnv } from "@ledgerhq/live-env";
6
+ import { ScenarioOptions } from "./types";
7
+ import { getSdk } from "../../src";
8
+ import { WithDevice } from "../../src/types";
9
+
10
+ setEnv("GET_CALLS_RETRY", 0);
11
+
12
+ export async function replayTrustchainSdkTests<Json extends JsonShape>(
13
+ json: Json,
14
+ scenario: (deviceId: string, scenarioOptions: ScenarioOptions) => Promise<void>,
15
+ ) {
16
+ // This replays, in order, all HTTP queries we have saved in json records
17
+ let httpTransactionIndex = 0;
18
+ const mockServer = setupServer(
19
+ http.all("*", async ({ request }) => {
20
+ const id = "http(" + httpTransactionIndex + "): ";
21
+ const expected = json.http.transactions[httpTransactionIndex++];
22
+ if (!expected) {
23
+ throw new Error("unexpected HTTP request has occured: " + request.url.toString());
24
+ }
25
+ expect(id + request.method + " " + request.url.toString()).toEqual(
26
+ id + expected.request.method + " " + expected.request.url,
27
+ );
28
+
29
+ expect({
30
+ url: request.url.toString(),
31
+ method: request.method,
32
+ body: request.body ? await request.text() : undefined,
33
+ headers: headersToJson(request.headers),
34
+ }).toEqual({
35
+ url: expected.request.url,
36
+ method: expected.request.method,
37
+ body: expected.request.body,
38
+ headers: expected.request.headers,
39
+ });
40
+
41
+ const response = new HttpResponse(expected.response.body, {
42
+ status: expected.response.status,
43
+ headers: expected.response.headers as Record<string, string>,
44
+ });
45
+ HttpResponse.json;
46
+ return response;
47
+ }),
48
+ );
49
+
50
+ // This replays, in order, all crypto randomKeypair we have saved in json records
51
+ let randomKeypairIndex = 0;
52
+ jest.spyOn(crypto, "randomKeypair").mockImplementation(() => {
53
+ const emits = json.crypto.randomKeypairOutputs[randomKeypairIndex++];
54
+ if (!emits) {
55
+ throw new Error("unexpected randomKeypair call");
56
+ }
57
+ const privateKey = crypto.from_hex(emits);
58
+ return crypto.keypairFromSecretKey(privateKey);
59
+ });
60
+
61
+ // This replays, in order, all crypto randomBytes we have saved in json records
62
+ let randomBytesIndex = 0;
63
+ jest.spyOn(crypto, "randomBytes").mockImplementation((size: number) => {
64
+ const emits = json.crypto.randomBytesOutputs[randomBytesIndex++];
65
+ if (!emits) {
66
+ throw new Error("unexpected randomBytes call");
67
+ }
68
+ const bytes = crypto.from_hex(emits);
69
+ if (bytes.length !== size) {
70
+ throw new Error("unexpected randomBytes size. Expected " + size + " but got " + bytes.length);
71
+ }
72
+ return Promise.resolve(bytes);
73
+ });
74
+
75
+ const recordStore = RecordStore.fromString(json.apdus);
76
+
77
+ mockServer.listen();
78
+ mockServer.resetHandlers();
79
+ try {
80
+ // This replays, in order, all APDUs we have saved in json records
81
+ const transport = await openTransportReplayer(recordStore);
82
+ const device = { id: "", transport };
83
+ const withDevice: WithDevice = () => fn => fn(device.transport);
84
+ const options: ScenarioOptions = {
85
+ withDevice,
86
+ sdkForName: name =>
87
+ getSdk(
88
+ !!getEnv("MOCK"),
89
+ { applicationId: 16, name, apiBaseUrl: getEnv("TRUSTCHAIN_API_STAGING") },
90
+ withDevice,
91
+ ),
92
+ pauseRecorder: () => Promise.resolve(), // replayer don't need to pause
93
+ switchDeviceSeed: async () => device, // nothing to actually do, we will continue replaying
94
+ };
95
+ await scenario(device.id, options);
96
+ } finally {
97
+ mockServer.close();
98
+ }
99
+
100
+ // verify we have consumed all the expected calls
101
+ expect({
102
+ httpCalls: httpTransactionIndex,
103
+ randomKeypairCalls: randomKeypairIndex,
104
+ randomBytesCalls: randomBytesIndex,
105
+ }).toEqual({
106
+ httpCalls: json.http.transactions.length,
107
+ randomKeypairCalls: json.crypto.randomKeypairOutputs.length,
108
+ randomBytesCalls: json.crypto.randomBytesOutputs.length,
109
+ });
110
+ }
111
+
112
+ type JsonShape = {
113
+ apdus: string;
114
+ http: {
115
+ transactions: {
116
+ request: {
117
+ url: string;
118
+ method: string;
119
+ body?: string;
120
+ headers: unknown;
121
+ };
122
+ response: {
123
+ status: number;
124
+ headers: unknown;
125
+ body?: string;
126
+ };
127
+ }[];
128
+ };
129
+ crypto: {
130
+ randomKeypairOutputs: string[];
131
+ randomBytesOutputs: string[];
132
+ };
133
+ };
134
+
135
+ function headersToJson(headers) {
136
+ const obj: Record<string, string> = {};
137
+ for (const [key, value] of headers) {
138
+ obj[key] = value;
139
+ }
140
+ return obj;
141
+ }
@@ -0,0 +1,45 @@
1
+ import { generateMnemonic } from "bip39";
2
+ import Transport from "@ledgerhq/hw-transport";
3
+ import { TrustchainSDK, WithDevice } from "../../src/types";
4
+
5
+ export type ScenarioOptions = {
6
+ /**
7
+ * easily create a sdk for a given member name
8
+ */
9
+ sdkForName: (name: string) => TrustchainSDK;
10
+ /**
11
+ * pause the recorder (e2e) part for a given amount of time
12
+ */
13
+ pauseRecorder: (milliseconds: number) => Promise<void>;
14
+ /**
15
+ * switch to the another device seed
16
+ */
17
+ switchDeviceSeed: (newSeed?: string) => Promise<{ id: string; transport: Transport }>;
18
+ /**
19
+ * withDevice function to pass to HWDeviceProvider
20
+ */
21
+ withDevice: WithDevice;
22
+ };
23
+
24
+ export type RecorderConfig = {
25
+ seed?: string;
26
+ coinapps?: string;
27
+ overridesAppPath?: string;
28
+ goNextOnText?: string[];
29
+ approveOnText?: string[];
30
+ approveOnceOnText?: string[];
31
+ };
32
+
33
+ export const recorderConfigDefaults = {
34
+ goNextOnText: [
35
+ "Login request",
36
+ "Identify with your",
37
+ "request",
38
+ "Ensure you trust the",
39
+ "keep",
40
+ "update request",
41
+ ],
42
+ approveOnText: ["Log in to", "Enable", "Confirm"],
43
+ };
44
+
45
+ export const genSeed = () => generateMnemonic(256);
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "esModuleInterop": true,
5
+ "lib": ["es2020"],
6
+ "types": ["jest"]
7
+ }
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "noImplicitAny": false,
7
+ "noImplicitThis": false,
8
+ "downlevelIteration": true,
9
+ "module": "commonjs",
10
+ "lib": ["es2020", "dom"],
11
+ "outDir": "lib"
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["src/__tests__/**/*"]
15
+ }