@ledgerhq/ledger-key-ring-protocol 0.5.1-fix-build-number-pre.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.
- package/.eslintrc.js +33 -0
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +299 -0
- package/LICENSE.txt +21 -0
- package/README.md +3 -0
- package/jest.config.js +13 -0
- package/lib/HWDeviceProvider.d.ts +25 -0
- package/lib/HWDeviceProvider.d.ts.map +1 -0
- package/lib/HWDeviceProvider.js +88 -0
- package/lib/HWDeviceProvider.js.map +1 -0
- package/lib/api.d.ts +77 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +150 -0
- package/lib/api.js.map +1 -0
- package/lib/auth.d.ts +3 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +79 -0
- package/lib/auth.js.map +1 -0
- package/lib/errors.d.ts +40 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +18 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/mockSdk.d.ts +22 -0
- package/lib/mockSdk.d.ts.map +1 -0
- package/lib/mockSdk.js +208 -0
- package/lib/mockSdk.js.map +1 -0
- package/lib/qrcode/cipher.d.ts +12 -0
- package/lib/qrcode/cipher.d.ts.map +1 -0
- package/lib/qrcode/cipher.js +52 -0
- package/lib/qrcode/cipher.js.map +1 -0
- package/lib/qrcode/cipher.test.d.ts +2 -0
- package/lib/qrcode/cipher.test.d.ts.map +1 -0
- package/lib/qrcode/cipher.test.js +31 -0
- package/lib/qrcode/cipher.test.js.map +1 -0
- package/lib/qrcode/index.d.ts +70 -0
- package/lib/qrcode/index.d.ts.map +1 -0
- package/lib/qrcode/index.js +309 -0
- package/lib/qrcode/index.js.map +1 -0
- package/lib/qrcode/index.test.d.ts +2 -0
- package/lib/qrcode/index.test.d.ts.map +1 -0
- package/lib/qrcode/index.test.js +131 -0
- package/lib/qrcode/index.test.js.map +1 -0
- package/lib/qrcode/types.d.ts +69 -0
- package/lib/qrcode/types.d.ts.map +1 -0
- package/lib/qrcode/types.js +3 -0
- package/lib/qrcode/types.js.map +1 -0
- package/lib/sdk.d.ts +31 -0
- package/lib/sdk.d.ts.map +1 -0
- package/lib/sdk.js +380 -0
- package/lib/sdk.js.map +1 -0
- package/lib/store.d.ts +71 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +62 -0
- package/lib/store.js.map +1 -0
- package/lib/types.d.ts +181 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +10 -0
- package/lib/types.js.map +1 -0
- package/lib-es/HWDeviceProvider.d.ts +25 -0
- package/lib-es/HWDeviceProvider.d.ts.map +1 -0
- package/lib-es/HWDeviceProvider.js +81 -0
- package/lib-es/HWDeviceProvider.js.map +1 -0
- package/lib-es/api.d.ts +77 -0
- package/lib-es/api.d.ts.map +1 -0
- package/lib-es/api.js +145 -0
- package/lib-es/api.js.map +1 -0
- package/lib-es/auth.d.ts +3 -0
- package/lib-es/auth.d.ts.map +1 -0
- package/lib-es/auth.js +75 -0
- package/lib-es/auth.js.map +1 -0
- package/lib-es/errors.d.ts +40 -0
- package/lib-es/errors.d.ts.map +1 -0
- package/lib-es/errors.js +15 -0
- package/lib-es/errors.js.map +1 -0
- package/lib-es/index.d.ts +6 -0
- package/lib-es/index.d.ts.map +1 -0
- package/lib-es/index.js +13 -0
- package/lib-es/index.js.map +1 -0
- package/lib-es/mockSdk.d.ts +22 -0
- package/lib-es/mockSdk.d.ts.map +1 -0
- package/lib-es/mockSdk.js +201 -0
- package/lib-es/mockSdk.js.map +1 -0
- package/lib-es/qrcode/cipher.d.ts +12 -0
- package/lib-es/qrcode/cipher.d.ts.map +1 -0
- package/lib-es/qrcode/cipher.js +44 -0
- package/lib-es/qrcode/cipher.js.map +1 -0
- package/lib-es/qrcode/cipher.test.d.ts +2 -0
- package/lib-es/qrcode/cipher.test.d.ts.map +1 -0
- package/lib-es/qrcode/cipher.test.js +29 -0
- package/lib-es/qrcode/cipher.test.js.map +1 -0
- package/lib-es/qrcode/index.d.ts +70 -0
- package/lib-es/qrcode/index.d.ts.map +1 -0
- package/lib-es/qrcode/index.js +301 -0
- package/lib-es/qrcode/index.js.map +1 -0
- package/lib-es/qrcode/index.test.d.ts +2 -0
- package/lib-es/qrcode/index.test.d.ts.map +1 -0
- package/lib-es/qrcode/index.test.js +126 -0
- package/lib-es/qrcode/index.test.js.map +1 -0
- package/lib-es/qrcode/types.d.ts +69 -0
- package/lib-es/qrcode/types.d.ts.map +1 -0
- package/lib-es/qrcode/types.js +2 -0
- package/lib-es/qrcode/types.js.map +1 -0
- package/lib-es/sdk.d.ts +31 -0
- package/lib-es/sdk.d.ts.map +1 -0
- package/lib-es/sdk.js +371 -0
- package/lib-es/sdk.js.map +1 -0
- package/lib-es/store.d.ts +71 -0
- package/lib-es/store.d.ts.map +1 -0
- package/lib-es/store.js +51 -0
- package/lib-es/store.js.map +1 -0
- package/lib-es/types.d.ts +181 -0
- package/lib-es/types.d.ts.map +1 -0
- package/lib-es/types.js +7 -0
- package/lib-es/types.js.map +1 -0
- package/mocks/scenarios/addSameMemberMultipleTimes.json +426 -0
- package/mocks/scenarios/create2trustchainInARow.json +616 -0
- package/mocks/scenarios/getOrCreateTransactionCases.json +591 -0
- package/mocks/scenarios/member3implicitlyAdded.json +648 -0
- package/mocks/scenarios/membersManySelfAdd.json +1427 -0
- package/mocks/scenarios/randomMemberTryToDestroy.json +371 -0
- package/mocks/scenarios/removeMemberWithTheWrongSeed.json +510 -0
- package/mocks/scenarios/removedMemberEjectedOnDeletedTrustchain.json +481 -0
- package/mocks/scenarios/removedMemberEjectedOnGetMembers.json +648 -0
- package/mocks/scenarios/removedMemberEjectedOnRestore.json +648 -0
- package/mocks/scenarios/removingAMemberCreatesAnInteraction.json +593 -0
- package/mocks/scenarios/removingYourselfIsForbidden.json +397 -0
- package/mocks/scenarios/success.json +978 -0
- package/mocks/scenarios/tokenExpires.json +371 -0
- package/mocks/scenarios/twoAddMembersFollowedByDeviceAdd.json +705 -0
- package/mocks/scenarios/userRefusesAuth.json +40 -0
- package/mocks/scenarios/userRefusesRemoveMember.json +542 -0
- package/package.json +91 -0
- package/scripts/README.md +15 -0
- package/scripts/e2e.ts +57 -0
- package/src/HWDeviceProvider.ts +105 -0
- package/src/__tests__/integration/mock.sdk.test.ts +47 -0
- package/src/__tests__/integration/sdk.test.ts +20 -0
- package/src/__tests__/tsconfig.json +8 -0
- package/src/__tests__/unit/sdk.test.ts +236 -0
- package/src/api.ts +202 -0
- package/src/auth.ts +81 -0
- package/src/errors.ts +18 -0
- package/src/index.ts +20 -0
- package/src/mockSdk.ts +253 -0
- package/src/qrcode/cipher.test.ts +30 -0
- package/src/qrcode/cipher.ts +61 -0
- package/src/qrcode/index.test.ts +138 -0
- package/src/qrcode/index.ts +394 -0
- package/src/qrcode/types.ts +70 -0
- package/src/sdk.ts +542 -0
- package/src/store.ts +99 -0
- package/src/types.ts +242 -0
- package/tests/scenarios/_template.ts +18 -0
- package/tests/scenarios/addSameMemberMultipleTimes.ts +20 -0
- package/tests/scenarios/create2trustchainInARow.ts +14 -0
- package/tests/scenarios/getOrCreateTransactionCases.ts +74 -0
- package/tests/scenarios/member3implicitlyAdded.ts +51 -0
- package/tests/scenarios/membersManySelfAdd.ts +18 -0
- package/tests/scenarios/randomMemberTryToDestroy.ts +23 -0
- package/tests/scenarios/removeMemberWithTheWrongSeed.ts +28 -0
- package/tests/scenarios/removedMemberEjectedOnDeletedTrustchain.ts +31 -0
- package/tests/scenarios/removedMemberEjectedOnGetMembers.ts +29 -0
- package/tests/scenarios/removedMemberEjectedOnRestore.ts +31 -0
- package/tests/scenarios/removingAMemberCreatesAnInteraction.ts +42 -0
- package/tests/scenarios/removingYourselfIsForbidden.ts +11 -0
- package/tests/scenarios/success.ts +94 -0
- package/tests/scenarios/tokenExpires.ts +20 -0
- package/tests/scenarios/twoAddMembersFollowedByDeviceAdd.ts +49 -0
- package/tests/scenarios/userRefusesAuth.ts +28 -0
- package/tests/scenarios/userRefusesRemoveMember.ts +66 -0
- package/tests/test-helpers/recordTrustchainSdkTests.ts +178 -0
- package/tests/test-helpers/replayTrustchainSdkTests.ts +141 -0
- package/tests/test-helpers/types.ts +45 -0
- package/tests/tsconfig.json +8 -0
- 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 = (size: number) => {
|
|
98
|
+
const bytes = 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 = () => {
|
|
107
|
+
const keypair = 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 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);
|
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
|
+
}
|