@ledgerhq/live-common 34.35.0-nightly.4 → 34.35.0-nightly.6

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 (49) hide show
  1. package/lib/e2e/enum/Account.d.ts +64 -58
  2. package/lib/e2e/enum/Account.d.ts.map +1 -1
  3. package/lib/e2e/enum/Account.js +80 -59
  4. package/lib/e2e/enum/Account.js.map +1 -1
  5. package/lib/e2e/models/BuySell.d.ts +11 -0
  6. package/lib/e2e/models/BuySell.d.ts.map +1 -0
  7. package/lib/e2e/models/BuySell.js +3 -0
  8. package/lib/e2e/models/BuySell.js.map +1 -0
  9. package/lib/e2e/models/Transaction.d.ts +5 -5
  10. package/lib/e2e/models/Transaction.d.ts.map +1 -1
  11. package/lib/e2e/models/Transaction.js.map +1 -1
  12. package/lib/e2e/speculos.d.ts +4 -1
  13. package/lib/e2e/speculos.d.ts.map +1 -1
  14. package/lib/e2e/speculos.js +24 -8
  15. package/lib/e2e/speculos.js.map +1 -1
  16. package/lib/e2e/speculosCI.d.ts +5 -0
  17. package/lib/e2e/speculosCI.d.ts.map +1 -0
  18. package/lib/e2e/speculosCI.js +129 -0
  19. package/lib/e2e/speculosCI.js.map +1 -0
  20. package/lib/hw/connectApp.js +1 -1
  21. package/lib/hw/connectApp.js.map +1 -1
  22. package/lib-es/e2e/enum/Account.d.ts +64 -58
  23. package/lib-es/e2e/enum/Account.d.ts.map +1 -1
  24. package/lib-es/e2e/enum/Account.js +76 -58
  25. package/lib-es/e2e/enum/Account.js.map +1 -1
  26. package/lib-es/e2e/models/BuySell.d.ts +11 -0
  27. package/lib-es/e2e/models/BuySell.d.ts.map +1 -0
  28. package/lib-es/e2e/models/BuySell.js +2 -0
  29. package/lib-es/e2e/models/BuySell.js.map +1 -0
  30. package/lib-es/e2e/models/Transaction.d.ts +5 -5
  31. package/lib-es/e2e/models/Transaction.d.ts.map +1 -1
  32. package/lib-es/e2e/models/Transaction.js.map +1 -1
  33. package/lib-es/e2e/speculos.d.ts +4 -1
  34. package/lib-es/e2e/speculos.d.ts.map +1 -1
  35. package/lib-es/e2e/speculos.js +24 -8
  36. package/lib-es/e2e/speculos.js.map +1 -1
  37. package/lib-es/e2e/speculosCI.d.ts +5 -0
  38. package/lib-es/e2e/speculosCI.d.ts.map +1 -0
  39. package/lib-es/e2e/speculosCI.js +121 -0
  40. package/lib-es/e2e/speculosCI.js.map +1 -0
  41. package/lib-es/hw/connectApp.js +1 -1
  42. package/lib-es/hw/connectApp.js.map +1 -1
  43. package/package.json +43 -43
  44. package/src/e2e/enum/Account.ts +358 -357
  45. package/src/e2e/models/BuySell.ts +12 -0
  46. package/src/e2e/models/Transaction.ts +5 -5
  47. package/src/e2e/speculos.ts +29 -10
  48. package/src/e2e/speculosCI.ts +161 -0
  49. package/src/hw/connectApp.ts +1 -1
@@ -0,0 +1,12 @@
1
+ import { AccountType } from "../enum/Account";
2
+
3
+ export interface Fiat {
4
+ locale: string;
5
+ currencyTicker: string;
6
+ }
7
+
8
+ export interface BuySell {
9
+ crypto: AccountType;
10
+ fiat: Fiat;
11
+ amount: string;
12
+ }
@@ -1,13 +1,13 @@
1
1
  import { Fee } from "../enum/Fee";
2
- import { Account } from "../enum/Account";
2
+ import { AccountType } from "../enum/Account";
3
3
  import { Nft } from "../enum/Nft";
4
4
 
5
5
  export type TransactionType = Transaction;
6
6
 
7
7
  export class Transaction {
8
8
  constructor(
9
- public accountToDebit: Account,
10
- public accountToCredit: Account,
9
+ public accountToDebit: AccountType,
10
+ public accountToCredit: AccountType,
11
11
  public amount: string,
12
12
  public speed?: Fee,
13
13
  public memoTag?: string,
@@ -16,8 +16,8 @@ export class Transaction {
16
16
 
17
17
  export class NFTTransaction extends Transaction {
18
18
  constructor(
19
- accountToDebit: Account,
20
- accountToCredit: Account,
19
+ accountToDebit: AccountType,
20
+ accountToCredit: AccountType,
21
21
  public nft: Nft,
22
22
  speed?: Fee,
23
23
  memoTag?: string,
@@ -7,7 +7,7 @@ import {
7
7
  findLatestAppCandidate,
8
8
  SpeculosTransport,
9
9
  } from "../load/speculos";
10
- import { SpeculosDevice } from "@ledgerhq/speculos-transport";
10
+ import { createSpeculosDeviceCI, releaseSpeculosDeviceCI } from "./speculosCI";
11
11
  import type { AppCandidate } from "@ledgerhq/coin-framework/bot/types";
12
12
  import { DeviceModelId } from "@ledgerhq/devices";
13
13
  import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
@@ -38,6 +38,8 @@ import { NFTTransaction, Transaction } from "./models/Transaction";
38
38
  import { Delegate } from "./models/Delegate";
39
39
  import { Swap } from "./models/Swap";
40
40
 
41
+ const isSpeculosRemote = process.env.REMOTE_SPECULOS === "true";
42
+
41
43
  export type Spec = {
42
44
  currency?: CryptoCurrency;
43
45
  appQuery: {
@@ -51,6 +53,10 @@ export type Spec = {
51
53
  };
52
54
 
53
55
  export type Dependency = { name: string; appVersion?: string };
56
+ export type SpeculosDevice = {
57
+ id: string;
58
+ port: number;
59
+ };
54
60
 
55
61
  export function setExchangeDependencies(dependencies: Dependency[]) {
56
62
  const map = new Map<string, Dependency>();
@@ -376,9 +382,14 @@ export async function startSpeculos(
376
382
  coinapps,
377
383
  onSpeculosDeviceCreated,
378
384
  };
379
-
380
385
  try {
381
- return await createSpeculosDevice(deviceParams);
386
+ const device = isSpeculosRemote
387
+ ? await createSpeculosDeviceCI(deviceParams)
388
+ : await createSpeculosDevice(deviceParams).then(device => {
389
+ invariant(device.ports.apiPort, "[E2E] Speculos apiPort is not defined");
390
+ return { id: device.id, port: device.ports.apiPort };
391
+ });
392
+ return device;
382
393
  } catch (e: unknown) {
383
394
  console.error(e);
384
395
  log("engine", `test ${testName} failed with ${String(e)}`);
@@ -388,7 +399,9 @@ export async function startSpeculos(
388
399
  export async function stopSpeculos(deviceId: string | undefined) {
389
400
  if (deviceId) {
390
401
  log("engine", `test ${deviceId} finished`);
391
- await releaseSpeculosDevice(deviceId);
402
+ isSpeculosRemote
403
+ ? await releaseSpeculosDeviceCI(deviceId)
404
+ : await releaseSpeculosDevice(deviceId);
392
405
  }
393
406
  }
394
407
 
@@ -402,11 +415,12 @@ interface ResponseData {
402
415
 
403
416
  export async function waitFor(text: string, maxAttempts: number = 10): Promise<string[]> {
404
417
  const speculosApiPort = getEnv("SPECULOS_API_PORT");
418
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
405
419
  let attempts = 0;
406
420
  let textFound: boolean = false;
407
421
  while (attempts < maxAttempts && !textFound) {
408
422
  const response = await axios.get<ResponseData>(
409
- `http://127.0.0.1:${speculosApiPort}/events?stream=false&currentscreenonly=true`,
423
+ `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=true`,
410
424
  );
411
425
  const responseData = response.data;
412
426
  const texts = responseData.events.map(event => event.text);
@@ -423,7 +437,8 @@ export async function waitFor(text: string, maxAttempts: number = 10): Promise<s
423
437
 
424
438
  export async function pressBoth() {
425
439
  const speculosApiPort = getEnv("SPECULOS_API_PORT");
426
- await axios.post(`http://127.0.0.1:${speculosApiPort}/button/both`, {
440
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
441
+ await axios.post(`${speculosAddress}:${speculosApiPort}/button/both`, {
427
442
  action: "press-and-release",
428
443
  });
429
444
  }
@@ -451,22 +466,25 @@ export async function pressUntilTextFound(
451
466
  }
452
467
 
453
468
  async function fetchCurrentScreenTexts(speculosApiPort: number): Promise<string> {
469
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
454
470
  const response = await axios.get<ResponseData>(
455
- `http://127.0.0.1:${speculosApiPort}/events?stream=false&currentscreenonly=true`,
471
+ `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=true`,
456
472
  );
457
473
  return response.data.events.map(event => event.text).join("");
458
474
  }
459
475
 
460
476
  async function fetchAllEvents(speculosApiPort: number): Promise<string[]> {
477
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
461
478
  const response = await axios.get<ResponseData>(
462
- `http://127.0.0.1:${speculosApiPort}/events?stream=false&currentscreenonly=false`,
479
+ `${speculosAddress}:${speculosApiPort}/events?stream=false&currentscreenonly=false`,
463
480
  );
464
481
  return response.data.events.map(event => event.text);
465
482
  }
466
483
 
467
484
  export async function pressRightButton(): Promise<void> {
468
485
  const speculosApiPort = getEnv("SPECULOS_API_PORT");
469
- await axios.post(`http://127.0.0.1:${speculosApiPort}/button/right`, {
486
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
487
+ await axios.post(`${speculosAddress}:${speculosApiPort}/button/right`, {
470
488
  action: "press-and-release",
471
489
  });
472
490
  }
@@ -486,9 +504,10 @@ export function containsSubstringInEvent(targetString: string, events: string[])
486
504
  }
487
505
 
488
506
  export async function takeScreenshot(port?: number): Promise<Buffer | undefined> {
507
+ const speculosAddress = process.env.SPECULOS_ADDRESS || "http://127.0.0.1";
489
508
  const speculosApiPort = port ?? getEnv("SPECULOS_API_PORT");
490
509
  try {
491
- const response = await axios.get(`http://127.0.0.1:${speculosApiPort}/screenshot`, {
510
+ const response = await axios.get(`${speculosAddress}:${speculosApiPort}/screenshot`, {
492
511
  responseType: "arraybuffer",
493
512
  });
494
513
  return response.data;
@@ -0,0 +1,161 @@
1
+ import axios from "axios";
2
+ import {
3
+ conventionalAppSubpath,
4
+ DeviceParams,
5
+ reverseModelMap,
6
+ } from "@ledgerhq/speculos-transport";
7
+ import { SpeculosDevice } from "./speculos";
8
+ import https from "https";
9
+
10
+ const { SEED, GITHUB_TOKEN, AWS_ROLE, CLUSTER } = process.env;
11
+ const GIT_API_URL = "https://api.github.com/repos/LedgerHQ/actions/actions/";
12
+ const START_WORKFLOW_ID = "workflows/161487603/dispatches";
13
+ const STOP_WORKFLOW_ID = "workflows/161487604/dispatches";
14
+ const GITHUB_REF = "main";
15
+ const getSpeculosAddress = (runId: string) => `https://${runId}.speculos.aws.stg.ldg-tech.com`;
16
+ const speculosPort = 443;
17
+
18
+ function uniqueId(): string {
19
+ const timestamp = Date.now().toString(36);
20
+ const randomString = Math.random().toString(36).slice(2, 7);
21
+ return timestamp + randomString;
22
+ }
23
+
24
+ /**
25
+ * Helper function to make API requests with error handling
26
+ */
27
+ async function githubApiRequest<T = unknown>({
28
+ method = "POST",
29
+ urlSuffix,
30
+ data,
31
+ params,
32
+ }: {
33
+ method?: "GET" | "POST";
34
+ urlSuffix: string;
35
+ data?: Record<string, unknown>;
36
+ params?: Record<string, unknown>;
37
+ }): Promise<T> {
38
+ const url = `${GIT_API_URL}${urlSuffix}`;
39
+ try {
40
+ const response = await axios({
41
+ method,
42
+ url,
43
+ headers: {
44
+ Authorization: `Bearer ${GITHUB_TOKEN}`,
45
+ Accept: "application/vnd.github+json",
46
+ "X-GitHub-Api-Version": "2022-11-28",
47
+ },
48
+ data,
49
+ params,
50
+ });
51
+ return response.data;
52
+ } catch (error) {
53
+ console.warn(
54
+ `API Request failed: ${method} ${url}`,
55
+ axios.isAxiosError(error) ? error.response?.data : (error as Error).message,
56
+ );
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ function waitForSpeculosReady(url: string, { interval = 2000, timeout = 120_000 } = {}) {
62
+ return new Promise((resolve, reject) => {
63
+ const startTime = Date.now();
64
+
65
+ function check() {
66
+ https
67
+ .get(url, res => {
68
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
69
+ process.env.SPECULOS_ADDRESS = url;
70
+ resolve(true);
71
+ } else {
72
+ retry();
73
+ }
74
+ })
75
+ .on("error", retry);
76
+ }
77
+
78
+ function retry() {
79
+ if (Date.now() - startTime >= timeout) {
80
+ reject(new Error(`Timeout: ${url} did not become available within ${timeout}ms`));
81
+ } else {
82
+ setTimeout(check, interval);
83
+ }
84
+ }
85
+
86
+ check();
87
+ });
88
+ }
89
+
90
+ function createStartPayload(deviceParams: DeviceParams, runId: string) {
91
+ const { model, firmware, appName, appVersion, dependency, dependencies } = deviceParams;
92
+
93
+ let additional_args = "";
94
+
95
+ if (dependency) {
96
+ additional_args = `-l ${dependency}:/apps/${conventionalAppSubpath(model, firmware, dependency, appVersion)}`;
97
+ } else if (dependencies) {
98
+ additional_args = [
99
+ ...new Set(
100
+ dependencies.map(
101
+ dep =>
102
+ `-l ${dep.name}:/apps/${conventionalAppSubpath(
103
+ model,
104
+ firmware,
105
+ dep.name,
106
+ dep.appVersion ?? "1.0.0",
107
+ )}`,
108
+ ),
109
+ ),
110
+ ].join(" ");
111
+ }
112
+
113
+ return {
114
+ ref: GITHUB_REF,
115
+ inputs: {
116
+ coin_app: appName,
117
+ coin_app_version: appVersion,
118
+ device: reverseModelMap[model],
119
+ device_os_version: firmware,
120
+ aws_role: AWS_ROLE,
121
+ cluster: CLUSTER,
122
+ seed: SEED,
123
+ run_id: runId,
124
+ additional_args,
125
+ },
126
+ };
127
+ }
128
+
129
+ export async function createSpeculosDeviceCI(
130
+ deviceParams: DeviceParams,
131
+ ): Promise<SpeculosDevice | undefined> {
132
+ try {
133
+ const runId = uniqueId();
134
+ console.warn("Creating remote speculos:", runId);
135
+ const data = createStartPayload(deviceParams, runId);
136
+ await githubApiRequest({ urlSuffix: START_WORKFLOW_ID, data });
137
+ await waitForSpeculosReady(getSpeculosAddress(runId));
138
+
139
+ return {
140
+ id: runId,
141
+ port: speculosPort,
142
+ };
143
+ } catch (e: unknown) {
144
+ console.error(e);
145
+ console.warn(
146
+ `Creating remote speculos ${deviceParams.appName}:${deviceParams.appVersion} failed with ${String(e)}`,
147
+ );
148
+ }
149
+ }
150
+
151
+ export async function releaseSpeculosDeviceCI(runId: string) {
152
+ const data = {
153
+ ref: GITHUB_REF,
154
+ inputs: {
155
+ run_id: runId.toString(),
156
+ aws_role: AWS_ROLE,
157
+ cluster: CLUSTER,
158
+ },
159
+ };
160
+ await githubApiRequest({ urlSuffix: STOP_WORKFLOW_ID, data });
161
+ }
@@ -534,7 +534,7 @@ export default function connectAppFactory(
534
534
  const deviceAction = new ConnectAppDeviceAction({
535
535
  input: {
536
536
  application: appNameToDependency(appName),
537
- dependencies: dependencies ? dependencies.map(name => appNameToDependency(name)) : [],
537
+ dependencies: dependencies ? dependencies.map(name => ({ name })) : [],
538
538
  requireLatestFirmware,
539
539
  allowMissingApplication: allowPartialDependencies,
540
540
  unlockTimeout: 0, // Expect to fail immediately when device is locked