@ledgerhq/live-common 34.55.0-nightly.20251213023821 → 34.55.0-nightly.20251215120904

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.
@@ -3,7 +3,7 @@
3
3
  exports[`crypto_org currency bridge scanAccounts crypto_org seed 1 1`] = `
4
4
  [
5
5
  {
6
- "balance": "10000009",
6
+ "balance": "10000010",
7
7
  "cosmosResources": {
8
8
  "delegatedBalance": "0",
9
9
  "delegations": [],
@@ -23,7 +23,7 @@ exports[`crypto_org currency bridge scanAccounts crypto_org seed 1 1`] = `
23
23
  "operationsCount": 0,
24
24
  "pendingOperations": [],
25
25
  "seedIdentifier": "0346289788c2f518a158232e7028e8e86b30cff0670326d3a2507183cb0d81cecb",
26
- "spendableBalance": "10000009",
26
+ "spendableBalance": "10000010",
27
27
  "swapHistory": [],
28
28
  "syncHash": undefined,
29
29
  "used": true,
@@ -3,7 +3,7 @@
3
3
  exports[`mantra currency bridge scanAccounts mantra seed 1 1`] = `
4
4
  [
5
5
  {
6
- "balance": "714929",
6
+ "balance": "714930",
7
7
  "currencyId": "mantra",
8
8
  "derivationMode": "",
9
9
  "freshAddress": "mantra1gyauvl44q2apn3u3aujm36q8zrj74vry7n5nvn",
@@ -12,7 +12,7 @@ exports[`mantra currency bridge scanAccounts mantra seed 1 1`] = `
12
12
  "index": 0,
13
13
  "pendingOperations": [],
14
14
  "seedIdentifier": "03d5e0ebb3f1ae2afe87e5d5a24b5029a59cc12f8fd1056840091b2f0b97e54e83",
15
- "spendableBalance": "204929",
15
+ "spendableBalance": "204930",
16
16
  "swapHistory": [],
17
17
  "syncHash": undefined,
18
18
  "used": true,
@@ -3,7 +3,7 @@
3
3
  exports[`osmo currency bridge scanAccounts osmo seed 1 1`] = `
4
4
  [
5
5
  {
6
- "balance": "124347",
6
+ "balance": "124348",
7
7
  "currencyId": "osmo",
8
8
  "derivationMode": "",
9
9
  "freshAddress": "osmo17gmcxyc5ccd5kwqqatpgfdgh380w2hc77zm0zw",
@@ -12,7 +12,7 @@ exports[`osmo currency bridge scanAccounts osmo seed 1 1`] = `
12
12
  "index": 0,
13
13
  "pendingOperations": [],
14
14
  "seedIdentifier": "02dc75cfe8137450ae2259dc7f29fd8767951956cc943adb4387e91c37a058a9f0",
15
- "spendableBalance": "124347",
15
+ "spendableBalance": "124348",
16
16
  "swapHistory": [],
17
17
  "syncHash": undefined,
18
18
  "used": true,
@@ -3,7 +3,7 @@
3
3
  exports[`persistence currency bridge scanAccounts persistence seed 1 1`] = `
4
4
  [
5
5
  {
6
- "balance": "2636576",
6
+ "balance": "2636577",
7
7
  "currencyId": "persistence",
8
8
  "derivationMode": "",
9
9
  "freshAddress": "persistence1gyauvl44q2apn3u3aujm36q8zrj74vrym5cypd",
@@ -12,14 +12,14 @@ exports[`persistence currency bridge scanAccounts persistence seed 1 1`] = `
12
12
  "index": 0,
13
13
  "pendingOperations": [],
14
14
  "seedIdentifier": "03d5e0ebb3f1ae2afe87e5d5a24b5029a59cc12f8fd1056840091b2f0b97e54e83",
15
- "spendableBalance": "1202572",
15
+ "spendableBalance": "1202573",
16
16
  "swapHistory": [],
17
17
  "syncHash": undefined,
18
18
  "used": true,
19
19
  "xpub": "persistence1gyauvl44q2apn3u3aujm36q8zrj74vrym5cypd",
20
20
  },
21
21
  {
22
- "balance": "230007",
22
+ "balance": "230008",
23
23
  "currencyId": "persistence",
24
24
  "derivationMode": "",
25
25
  "freshAddress": "persistence1v2mp0m7k96dm9qv60fkspcqlzpkzrwnevf47z2",
@@ -28,7 +28,7 @@ exports[`persistence currency bridge scanAccounts persistence seed 1 1`] = `
28
28
  "index": 1,
29
29
  "pendingOperations": [],
30
30
  "seedIdentifier": "03d5e0ebb3f1ae2afe87e5d5a24b5029a59cc12f8fd1056840091b2f0b97e54e83",
31
- "spendableBalance": "230007",
31
+ "spendableBalance": "230008",
32
32
  "swapHistory": [],
33
33
  "syncHash": undefined,
34
34
  "used": true,
@@ -1,7 +1,7 @@
1
1
  import { TestScheduler } from "rxjs/testing";
2
2
  import installApp from "./installApp";
3
3
  import ManagerAPI from "../manager/api";
4
- import { defer } from "rxjs";
4
+ import { defer, of, throwError } from "rxjs";
5
5
  import { anAppBuilder } from "../mock/fixtures/anApp";
6
6
  import { aTransportBuilder } from "@ledgerhq/hw-transport-mocker";
7
7
  import {
@@ -9,6 +9,7 @@ import {
9
9
  ManagerDeviceLockedError,
10
10
  UnresponsiveDeviceError,
11
11
  } from "@ledgerhq/errors";
12
+ import { quitApp } from "../deviceSDK/commands/quitApp";
12
13
 
13
14
  // Mocking ManagerAPI
14
15
  jest.mock("../manager/api", () => {
@@ -19,7 +20,13 @@ jest.mock("../manager/api", () => {
19
20
  install: jest.fn(),
20
21
  };
21
22
  });
23
+
24
+ jest.mock("../deviceSDK/commands/quitApp", () => ({
25
+ quitApp: jest.fn(),
26
+ }));
27
+
22
28
  const mockManagerApiInstall = jest.mocked(ManagerAPI.install);
29
+ const mockQuitApp = jest.mocked(quitApp);
23
30
 
24
31
  describe("installApp", () => {
25
32
  let testScheduler: TestScheduler;
@@ -29,6 +36,11 @@ describe("installApp", () => {
29
36
  // Makes TestScheduler assertions works with our test framework Jest
30
37
  expect(actual).toEqual(expected);
31
38
  });
39
+
40
+ mockManagerApiInstall.mockReset();
41
+ mockQuitApp.mockReset();
42
+
43
+ mockQuitApp.mockReturnValue(of<void>(undefined));
32
44
  });
33
45
 
34
46
  test("On one error followed by a successfully emitted event, it should retry after the given delay", () => {
@@ -149,4 +161,73 @@ describe("installApp", () => {
149
161
  });
150
162
  }
151
163
  });
164
+
165
+ test("it should run quit app at the beginning then keep going with manager", () => {
166
+ const transport = aTransportBuilder();
167
+ const app = anAppBuilder();
168
+
169
+ const fakeEvent = {
170
+ type: "bulk-progress",
171
+ progress: 0.5,
172
+ };
173
+
174
+ mockManagerApiInstall.mockReturnValue(of(fakeEvent));
175
+
176
+ const results: Array<{ progress: number }> = [];
177
+
178
+ installApp(transport, "target-test", app).subscribe({
179
+ next: v => results.push(v),
180
+ error: e => {
181
+ throw e;
182
+ },
183
+ });
184
+
185
+ // quitApp called once, with the same transport
186
+ expect(mockQuitApp).toHaveBeenCalledTimes(1);
187
+ expect(mockQuitApp).toHaveBeenCalledWith(transport);
188
+
189
+ // ManagerAPI.install called once, with correct params
190
+ expect(mockManagerApiInstall).toHaveBeenCalledTimes(1);
191
+ expect(mockManagerApiInstall).toHaveBeenCalledWith(
192
+ transport,
193
+ "install-app",
194
+ expect.objectContaining({
195
+ targetId: "target-test",
196
+ perso: app.perso,
197
+ deleteKey: app.delete_key,
198
+ firmware: app.firmware,
199
+ firmwareKey: app.firmware_key,
200
+ hash: app.hash,
201
+ }),
202
+ );
203
+
204
+ // quitApp must be invoked before ManagerAPI.install
205
+ expect(mockQuitApp.mock.invocationCallOrder[0]).toBeLessThan(
206
+ mockManagerApiInstall.mock.invocationCallOrder[0],
207
+ );
208
+
209
+ expect(results).toEqual([{ progress: 0.5 }]);
210
+ });
211
+
212
+ test("it should return error if quit app fail", done => {
213
+ const transport = aTransportBuilder();
214
+ const app = anAppBuilder();
215
+ const error = new Error("quit failed");
216
+
217
+ mockQuitApp.mockReturnValueOnce(throwError(() => error));
218
+
219
+ const observable = installApp(transport, "target-test", app);
220
+
221
+ observable.subscribe({
222
+ next: () => {
223
+ done(new Error("Expected an error, but got a next notification"));
224
+ },
225
+ error: err => {
226
+ expect(err).toBe(error);
227
+ // ManagerAPI.install should never be called if quitApp fails
228
+ expect(mockManagerApiInstall).not.toHaveBeenCalled();
229
+ done();
230
+ },
231
+ });
232
+ });
152
233
  });
@@ -1,5 +1,5 @@
1
1
  import { Observable, throwError, timer } from "rxjs";
2
- import { throttleTime, filter, map, catchError, retry } from "rxjs/operators";
2
+ import { throttleTime, filter, map, catchError, retry, switchMap } from "rxjs/operators";
3
3
  import {
4
4
  LockedDeviceError,
5
5
  ManagerAppDepInstallRequired,
@@ -12,6 +12,7 @@ import ManagerAPI from "../manager/api";
12
12
  import { getDependencies } from "../apps/polyfill";
13
13
  import { LocalTracer } from "@ledgerhq/logs";
14
14
  import { LOG_TYPE } from ".";
15
+ import { quitApp } from "../deviceSDK/commands/quitApp";
15
16
 
16
17
  const APP_INSTALL_RETRY_DELAY = 500;
17
18
  const APP_INSTALL_RETRY_LIMIT = 5;
@@ -53,60 +54,65 @@ export default function installApp(
53
54
  retryDelayMs,
54
55
  });
55
56
 
56
- return ManagerAPI.install(transport, "install-app", {
57
- targetId,
58
- perso: app.perso,
59
- deleteKey: app.delete_key,
60
- firmware: app.firmware,
61
- firmwareKey: app.firmware_key,
62
- hash: app.hash,
63
- }).pipe(
64
- retry({
65
- count: retryLimit,
66
- delay: (error: unknown, retryCount: number) => {
67
- // Not retrying on locked device errors
68
- if (
69
- error instanceof LockedDeviceError ||
70
- error instanceof ManagerDeviceLockedError ||
71
- error instanceof UnresponsiveDeviceError
72
- ) {
73
- tracer.trace(`Not retrying on error: ${error}`, {
74
- error,
75
- });
76
- return throwError(() => error);
77
- }
57
+ // Run quitApp just before ManagerAPI.install
58
+ return quitApp(transport).pipe(
59
+ switchMap(() =>
60
+ ManagerAPI.install(transport, "install-app", {
61
+ targetId,
62
+ perso: app.perso,
63
+ deleteKey: app.delete_key,
64
+ firmware: app.firmware,
65
+ firmwareKey: app.firmware_key,
66
+ hash: app.hash,
67
+ }).pipe(
68
+ retry({
69
+ count: retryLimit,
70
+ delay: (error: unknown, retryCount: number) => {
71
+ // Not retrying on locked device errors
72
+ if (
73
+ error instanceof LockedDeviceError ||
74
+ error instanceof ManagerDeviceLockedError ||
75
+ error instanceof UnresponsiveDeviceError
76
+ ) {
77
+ tracer.trace(`Not retrying on error: ${error}`, {
78
+ error,
79
+ });
80
+ return throwError(() => error);
81
+ }
78
82
 
79
- tracer.trace(`Retrying (${retryCount}/${retryLimit}) on error: ${error}`, {
80
- error,
81
- retryLimit,
82
- retryDelayMs,
83
- });
84
- return timer(retryDelayMs);
85
- },
86
- }),
87
- filter((e: any) => e.type === "bulk-progress"), // only bulk progress interests the UI
88
- throttleTime(100), // throttle to only emit 10 event/s max, to not spam the UI
89
- map((e: any) => ({
90
- progress: e.progress,
91
- })), // extract a stream of progress percentage
92
- catchError((e: Error) => {
93
- tracer.trace(`Error: ${e}`, { error: e });
83
+ tracer.trace(`Retrying (${retryCount}/${retryLimit}) on error: ${error}`, {
84
+ error,
85
+ retryLimit,
86
+ retryDelayMs,
87
+ });
88
+ return timer(retryDelayMs);
89
+ },
90
+ }),
91
+ filter((e: any) => e.type === "bulk-progress"), // only bulk progress interests the UI
92
+ throttleTime(100), // throttle to only emit 10 event/s max, to not spam the UI
93
+ map((e: any) => ({
94
+ progress: e.progress,
95
+ })), // extract a stream of progress percentage
96
+ catchError((e: Error) => {
97
+ tracer.trace(`Error: ${e}`, { error: e });
94
98
 
95
- if (!e || !e.message) return throwError(() => e);
99
+ if (!e || !e.message) return throwError(() => e);
96
100
 
97
- const status = e.message.slice(e.message.length - 4);
98
- if (status === "6a83" || status === "6811") {
99
- const dependencies = getDependencies(app.name);
100
- return throwError(
101
- () =>
102
- new ManagerAppDepInstallRequired("", {
103
- appName: app.name,
104
- dependency: dependencies.join(", "),
105
- }),
106
- );
107
- }
101
+ const status = e.message.slice(e.message.length - 4);
102
+ if (status === "6a83" || status === "6811") {
103
+ const dependencies = getDependencies(app.name);
104
+ return throwError(
105
+ () =>
106
+ new ManagerAppDepInstallRequired("", {
107
+ appName: app.name,
108
+ dependency: dependencies.join(", "),
109
+ }),
110
+ );
111
+ }
108
112
 
109
- return throwError(() => e);
110
- }),
113
+ return throwError(() => e);
114
+ }),
115
+ ),
116
+ ),
111
117
  );
112
118
  }