@ledgerhq/coin-framework 6.19.0 → 6.20.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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +70 -0
- package/lib/api/types.d.ts +45 -9
- package/lib/api/types.d.ts.map +1 -1
- package/lib/bridge/jsHelpers.d.ts +3 -2
- package/lib/bridge/jsHelpers.d.ts.map +1 -1
- package/lib/bridge/jsHelpers.js +132 -80
- package/lib/bridge/jsHelpers.js.map +1 -1
- package/lib/operation.d.ts +6 -0
- package/lib/operation.d.ts.map +1 -1
- package/lib/operation.js +30 -2
- package/lib/operation.js.map +1 -1
- package/lib-es/api/types.d.ts +45 -9
- package/lib-es/api/types.d.ts.map +1 -1
- package/lib-es/bridge/jsHelpers.d.ts +3 -2
- package/lib-es/bridge/jsHelpers.d.ts.map +1 -1
- package/lib-es/bridge/jsHelpers.js +133 -81
- package/lib-es/bridge/jsHelpers.js.map +1 -1
- package/lib-es/operation.d.ts +6 -0
- package/lib-es/operation.d.ts.map +1 -1
- package/lib-es/operation.js +28 -1
- package/lib-es/operation.js.map +1 -1
- package/package.json +16 -16
- package/src/api/types.ts +58 -16
- package/src/bridge/jsHelpers.test.ts +371 -1
- package/src/bridge/jsHelpers.ts +193 -118
- package/src/operation.test.ts +70 -1
- package/src/operation.ts +29 -1
- package/tsconfig.build.json +15 -0
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
TransactionCommon,
|
|
8
8
|
} from "@ledgerhq/types-live";
|
|
9
9
|
import BigNumber from "bignumber.js";
|
|
10
|
-
import { firstValueFrom } from "rxjs";
|
|
10
|
+
import { firstValueFrom, Observable, of, Subscription, throwError } from "rxjs";
|
|
11
11
|
import {
|
|
12
12
|
AccountShapeInfo,
|
|
13
13
|
bip32asBuffer,
|
|
@@ -64,6 +64,10 @@ describe("makeSync", () => {
|
|
|
64
64
|
// When
|
|
65
65
|
const accountUpdater = makeSync({
|
|
66
66
|
getAccountShape: (_accountShape: AccountShapeInfo) => Promise.resolve({} as Account),
|
|
67
|
+
postSync: (initial: Account, acc: Account) => ({
|
|
68
|
+
...acc,
|
|
69
|
+
lastSyncDate: new Date("2024-05-12T17:04:42"),
|
|
70
|
+
}),
|
|
67
71
|
})(account, {} as SyncConfig);
|
|
68
72
|
const updater = await firstValueFrom(accountUpdater);
|
|
69
73
|
const newAccount = updater(account);
|
|
@@ -145,6 +149,166 @@ describe("makeSync", () => {
|
|
|
145
149
|
// Then
|
|
146
150
|
expect(newAccount.id).toEqual(account.id);
|
|
147
151
|
});
|
|
152
|
+
|
|
153
|
+
it("supports getAccountShape returning an Observable", async () => {
|
|
154
|
+
const account = createAccount({
|
|
155
|
+
id: "12",
|
|
156
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
157
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
158
|
+
});
|
|
159
|
+
const accountUpdater = makeSync({
|
|
160
|
+
getAccountShape: () => of({} as Account),
|
|
161
|
+
})(account, {} as SyncConfig);
|
|
162
|
+
const updater = await firstValueFrom(accountUpdater);
|
|
163
|
+
const newAccount = updater(account);
|
|
164
|
+
expect(newAccount).toBeDefined();
|
|
165
|
+
expect(newAccount.id).not.toEqual(account.id);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("emits multiple updater events when getAccountShape Observable emits multiple shapes", async () => {
|
|
169
|
+
const account = createAccount({
|
|
170
|
+
id: "multi",
|
|
171
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
172
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
173
|
+
});
|
|
174
|
+
const sync$ = makeSync({
|
|
175
|
+
getAccountShape: () => of({} as Account, {} as Account, {} as Account),
|
|
176
|
+
})(account, {} as SyncConfig);
|
|
177
|
+
const updaters: Array<(a: Account) => Account> = [];
|
|
178
|
+
await new Promise<void>((resolve, reject) => {
|
|
179
|
+
let sub: ReturnType<typeof sync$.subscribe> | null = null;
|
|
180
|
+
sub = sync$.subscribe({
|
|
181
|
+
next: updater => updaters.push(updater),
|
|
182
|
+
error: err => {
|
|
183
|
+
sub?.unsubscribe();
|
|
184
|
+
reject(err);
|
|
185
|
+
},
|
|
186
|
+
complete: () => resolve(),
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
expect(updaters).toHaveLength(3);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("tears down inner subscription when sync Observable is unsubscribed", async () => {
|
|
193
|
+
const account = createAccount({
|
|
194
|
+
id: "12",
|
|
195
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
196
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
197
|
+
});
|
|
198
|
+
let resolveShape: (value: Partial<Account>) => void;
|
|
199
|
+
const shapePromise = new Promise<Partial<Account>>(resolve => {
|
|
200
|
+
resolveShape = resolve;
|
|
201
|
+
});
|
|
202
|
+
const nextSpy = jest.fn();
|
|
203
|
+
const completeSpy = jest.fn();
|
|
204
|
+
const sync$ = makeSync({
|
|
205
|
+
getAccountShape: () => shapePromise,
|
|
206
|
+
})(account, {} as SyncConfig);
|
|
207
|
+
const sub = sync$.subscribe({
|
|
208
|
+
next: nextSpy,
|
|
209
|
+
complete: completeSpy,
|
|
210
|
+
});
|
|
211
|
+
sub.unsubscribe();
|
|
212
|
+
resolveShape!({} as Account);
|
|
213
|
+
await new Promise(r => setImmediate(r));
|
|
214
|
+
expect(nextSpy).not.toHaveBeenCalled();
|
|
215
|
+
expect(completeSpy).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("does not forward observer error when unsubscribed before shape errors", async () => {
|
|
219
|
+
const account = createAccount({
|
|
220
|
+
id: "12",
|
|
221
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
222
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
223
|
+
});
|
|
224
|
+
let rejectShape: (reason: unknown) => void;
|
|
225
|
+
const shapePromise = new Promise<Partial<Account>>((_, reject) => {
|
|
226
|
+
rejectShape = reject;
|
|
227
|
+
});
|
|
228
|
+
const errorSpy = jest.fn();
|
|
229
|
+
const sync$ = makeSync({
|
|
230
|
+
getAccountShape: () => shapePromise,
|
|
231
|
+
})(account, {} as SyncConfig);
|
|
232
|
+
const sub = sync$.subscribe({ error: errorSpy });
|
|
233
|
+
sub.unsubscribe();
|
|
234
|
+
rejectShape!(new Error("shape error"));
|
|
235
|
+
await new Promise(r => setImmediate(r));
|
|
236
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("does not forward getAccountShape rejection when already unsubscribed", async () => {
|
|
240
|
+
const account = createAccount({
|
|
241
|
+
id: "12",
|
|
242
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
243
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
244
|
+
});
|
|
245
|
+
let rejectGetAccountShape: (reason: unknown) => void;
|
|
246
|
+
const getAccountShapePromise = new Promise<Partial<Account>>((_, reject) => {
|
|
247
|
+
rejectGetAccountShape = reject;
|
|
248
|
+
});
|
|
249
|
+
const errorSpy = jest.fn();
|
|
250
|
+
const sync$ = makeSync({
|
|
251
|
+
getAccountShape: () => getAccountShapePromise,
|
|
252
|
+
})(account, {} as SyncConfig);
|
|
253
|
+
const sub = sync$.subscribe({ error: errorSpy });
|
|
254
|
+
sub.unsubscribe();
|
|
255
|
+
rejectGetAccountShape!(new Error("getAccountShape failed"));
|
|
256
|
+
await new Promise(r => setImmediate(r));
|
|
257
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("unsubscribes inner subscription when cancelled during shape subscribe", async () => {
|
|
261
|
+
const account = createAccount({
|
|
262
|
+
id: "12",
|
|
263
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
264
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
265
|
+
});
|
|
266
|
+
const nextSpy = jest.fn();
|
|
267
|
+
const completeSpy = jest.fn();
|
|
268
|
+
const errorSpy = jest.fn();
|
|
269
|
+
// eslint-disable-next-line prefer-const
|
|
270
|
+
let sub: Subscription;
|
|
271
|
+
const sync$ = makeSync({
|
|
272
|
+
getAccountShape: () =>
|
|
273
|
+
new Observable<Partial<Account>>(() => {
|
|
274
|
+
setImmediate(() => sub?.unsubscribe());
|
|
275
|
+
}),
|
|
276
|
+
})(account, {} as SyncConfig);
|
|
277
|
+
sub = sync$.subscribe({
|
|
278
|
+
next: nextSpy,
|
|
279
|
+
complete: completeSpy,
|
|
280
|
+
error: errorSpy,
|
|
281
|
+
});
|
|
282
|
+
await new Promise(r => setImmediate(r));
|
|
283
|
+
expect(nextSpy).not.toHaveBeenCalled();
|
|
284
|
+
expect(completeSpy).not.toHaveBeenCalled();
|
|
285
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("errors when getAccountShape rejects", async () => {
|
|
289
|
+
const account = createAccount({
|
|
290
|
+
id: "12",
|
|
291
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
292
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
293
|
+
});
|
|
294
|
+
const sync$ = makeSync({
|
|
295
|
+
getAccountShape: () => Promise.reject(new Error("getAccountShape failed")),
|
|
296
|
+
})(account, {} as SyncConfig);
|
|
297
|
+
await expect(firstValueFrom(sync$)).rejects.toThrow("getAccountShape failed");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("errors when getAccountShape returns an Observable that errors", async () => {
|
|
301
|
+
const account = createAccount({
|
|
302
|
+
id: "12",
|
|
303
|
+
creationDate: new Date("2024-05-12T17:04:12"),
|
|
304
|
+
lastSyncDate: new Date("2024-05-12T17:04:12"),
|
|
305
|
+
});
|
|
306
|
+
const sync$ = makeSync({
|
|
307
|
+
getAccountShape: (_accountShape, _config) =>
|
|
308
|
+
throwError(() => new Error("Observable shape error")),
|
|
309
|
+
})(account, {} as SyncConfig);
|
|
310
|
+
await expect(firstValueFrom(sync$)).rejects.toThrow("Observable shape error");
|
|
311
|
+
});
|
|
148
312
|
});
|
|
149
313
|
|
|
150
314
|
describe("makeScanAccounts", () => {
|
|
@@ -490,6 +654,212 @@ describe("makeScanAccounts", () => {
|
|
|
490
654
|
},
|
|
491
655
|
});
|
|
492
656
|
});
|
|
657
|
+
|
|
658
|
+
it("emits discovered event before complete", async () => {
|
|
659
|
+
const addressResolver = {
|
|
660
|
+
address: "address",
|
|
661
|
+
path: "path",
|
|
662
|
+
publicKey: "publicKey",
|
|
663
|
+
};
|
|
664
|
+
const currency = getCryptoCurrencyById("algorand");
|
|
665
|
+
const events: Array<{ type: string; account?: Account }> = [];
|
|
666
|
+
const scanAccounts = makeScanAccounts({
|
|
667
|
+
getAccountShape: info =>
|
|
668
|
+
Promise.resolve({
|
|
669
|
+
id: `acc-${info.index}`,
|
|
670
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
671
|
+
}),
|
|
672
|
+
getAddressFn: () => Promise.resolve(addressResolver),
|
|
673
|
+
});
|
|
674
|
+
await new Promise<void>((resolve, reject) => {
|
|
675
|
+
scanAccounts({
|
|
676
|
+
currency,
|
|
677
|
+
deviceId: "deviceId",
|
|
678
|
+
syncConfig: { paginationConfig: {} },
|
|
679
|
+
}).subscribe({
|
|
680
|
+
next: event => events.push(event),
|
|
681
|
+
complete: () => resolve(),
|
|
682
|
+
error: reject,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
const discovered = events.filter(e => e.type === "discovered");
|
|
686
|
+
expect(discovered.length).toBeGreaterThan(0);
|
|
687
|
+
expect(discovered.map(e => e.account?.id)).toContain("acc-0");
|
|
688
|
+
const lastEvent = events[events.length - 1];
|
|
689
|
+
expect(lastEvent.type).toBe("discovered");
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("accepts Observable from getAccountShape", async () => {
|
|
693
|
+
const addressResolver = {
|
|
694
|
+
address: "address",
|
|
695
|
+
path: "path",
|
|
696
|
+
publicKey: "publicKey",
|
|
697
|
+
};
|
|
698
|
+
const currency = getCryptoCurrencyById("algorand");
|
|
699
|
+
const scanAccounts = makeScanAccounts({
|
|
700
|
+
getAccountShape: () =>
|
|
701
|
+
of({
|
|
702
|
+
id: "obs-acc-id",
|
|
703
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
704
|
+
}),
|
|
705
|
+
getAddressFn: () => Promise.resolve(addressResolver),
|
|
706
|
+
});
|
|
707
|
+
const result = await firstValueFrom(
|
|
708
|
+
scanAccounts({
|
|
709
|
+
currency,
|
|
710
|
+
deviceId: "deviceId",
|
|
711
|
+
syncConfig: { paginationConfig: {} },
|
|
712
|
+
}),
|
|
713
|
+
);
|
|
714
|
+
expect(result.type).toBe("discovered");
|
|
715
|
+
expect(result.account.id).toBe("obs-acc-id");
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("handles multi-emission Observable from getAccountShape", async () => {
|
|
719
|
+
const addressResolver = {
|
|
720
|
+
address: "address",
|
|
721
|
+
path: "path",
|
|
722
|
+
publicKey: "publicKey",
|
|
723
|
+
};
|
|
724
|
+
const currency = getCryptoCurrencyById("algorand");
|
|
725
|
+
const events: Array<{ type: string; account?: Account }> = [];
|
|
726
|
+
const scanAccounts = makeScanAccounts({
|
|
727
|
+
getAccountShape: (info: AccountShapeInfo) =>
|
|
728
|
+
new Observable(subscriber => {
|
|
729
|
+
subscriber.next({
|
|
730
|
+
id: `multi-obs-acc-${info.index}-intermediate`,
|
|
731
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
732
|
+
});
|
|
733
|
+
subscriber.next({
|
|
734
|
+
id: `multi-obs-acc-${info.index}-final`,
|
|
735
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
736
|
+
});
|
|
737
|
+
subscriber.complete();
|
|
738
|
+
}),
|
|
739
|
+
getAddressFn: () => Promise.resolve(addressResolver),
|
|
740
|
+
});
|
|
741
|
+
await new Promise<void>((resolve, reject) => {
|
|
742
|
+
scanAccounts({
|
|
743
|
+
currency,
|
|
744
|
+
deviceId: "deviceId",
|
|
745
|
+
syncConfig: { paginationConfig: {} },
|
|
746
|
+
}).subscribe({
|
|
747
|
+
next: event => events.push(event),
|
|
748
|
+
complete: () => resolve(),
|
|
749
|
+
error: reject,
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
const discovered = events.filter(e => e.type === "discovered");
|
|
753
|
+
expect(discovered.length).toBeGreaterThan(0);
|
|
754
|
+
const firstDiscovered = discovered[0];
|
|
755
|
+
expect(firstDiscovered.account?.id).toBe("multi-obs-acc-0-final");
|
|
756
|
+
});
|
|
757
|
+
it("propagates error when getAccountShape Observable errors", async () => {
|
|
758
|
+
const addressResolver = {
|
|
759
|
+
address: "address",
|
|
760
|
+
path: "path",
|
|
761
|
+
publicKey: "publicKey",
|
|
762
|
+
};
|
|
763
|
+
const currency = getCryptoCurrencyById("algorand");
|
|
764
|
+
const scanAccounts = makeScanAccounts({
|
|
765
|
+
getAccountShape: () => throwError(() => new Error("Observable shape error")),
|
|
766
|
+
getAddressFn: () => Promise.resolve(addressResolver),
|
|
767
|
+
});
|
|
768
|
+
await expect(
|
|
769
|
+
firstValueFrom(
|
|
770
|
+
scanAccounts({
|
|
771
|
+
currency,
|
|
772
|
+
deviceId: "deviceId",
|
|
773
|
+
syncConfig: { paginationConfig: {} },
|
|
774
|
+
}),
|
|
775
|
+
),
|
|
776
|
+
).rejects.toThrow("Observable shape error");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("when empty account skip is hit, continues to next derivation mode and still emits accounts", async () => {
|
|
780
|
+
const currency = getCryptoCurrencyById("ethereum");
|
|
781
|
+
const resolversByPath: Record<string, { address: string; path: string; publicKey: string }> =
|
|
782
|
+
{};
|
|
783
|
+
const getAddressFn = jest.fn((_deviceId: string, opts: { path: string }) => {
|
|
784
|
+
const path = opts.path;
|
|
785
|
+
if (!resolversByPath[path]) {
|
|
786
|
+
resolversByPath[path] = {
|
|
787
|
+
address: `0x${path.slice(-8)}`,
|
|
788
|
+
path,
|
|
789
|
+
publicKey: `pk-${path}`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return Promise.resolve(resolversByPath[path]);
|
|
793
|
+
});
|
|
794
|
+
const scanAccounts = makeScanAccounts({
|
|
795
|
+
getAccountShape: info => {
|
|
796
|
+
if (info.derivationMode === "ethM") {
|
|
797
|
+
return Promise.resolve({
|
|
798
|
+
id: `ethM-${info.index}`,
|
|
799
|
+
used: false,
|
|
800
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return Promise.resolve({
|
|
804
|
+
id: "ethMM-account",
|
|
805
|
+
used: true,
|
|
806
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
807
|
+
});
|
|
808
|
+
},
|
|
809
|
+
getAddressFn,
|
|
810
|
+
});
|
|
811
|
+
const events: Array<{ type: string; account?: Account }> = [];
|
|
812
|
+
await new Promise<void>((resolve, reject) => {
|
|
813
|
+
scanAccounts({
|
|
814
|
+
currency,
|
|
815
|
+
deviceId: "deviceId",
|
|
816
|
+
syncConfig: { paginationConfig: {} },
|
|
817
|
+
}).subscribe({
|
|
818
|
+
next: e => events.push(e),
|
|
819
|
+
complete: () => resolve(),
|
|
820
|
+
error: reject,
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
const discovered = events.filter(e => e.type === "discovered");
|
|
824
|
+
expect(discovered.length).toBeGreaterThanOrEqual(1);
|
|
825
|
+
const ethMMAccount = discovered.find(e => e.account?.id === "ethMM-account");
|
|
826
|
+
expect(ethMMAccount).toBeDefined();
|
|
827
|
+
expect(ethMMAccount!.account!.used).toBe(true);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("does not call complete when subscription is unsubscribed before scan finishes", async () => {
|
|
831
|
+
const addressResolver = {
|
|
832
|
+
address: "address",
|
|
833
|
+
path: "path",
|
|
834
|
+
publicKey: "publicKey",
|
|
835
|
+
};
|
|
836
|
+
const currency = getCryptoCurrencyById("algorand");
|
|
837
|
+
let resolveFirstShape: (value: Partial<Account>) => void;
|
|
838
|
+
const firstShapePromise = new Promise<Partial<Account>>(resolve => {
|
|
839
|
+
resolveFirstShape = resolve;
|
|
840
|
+
});
|
|
841
|
+
const completeSpy = jest.fn();
|
|
842
|
+
const scanAccounts = makeScanAccounts({
|
|
843
|
+
getAccountShape: () => firstShapePromise,
|
|
844
|
+
getAddressFn: () => Promise.resolve(addressResolver),
|
|
845
|
+
});
|
|
846
|
+
const sub = scanAccounts({
|
|
847
|
+
currency,
|
|
848
|
+
deviceId: "deviceId",
|
|
849
|
+
syncConfig: { paginationConfig: {} },
|
|
850
|
+
}).subscribe({
|
|
851
|
+
next: () => {},
|
|
852
|
+
complete: completeSpy,
|
|
853
|
+
error: () => {},
|
|
854
|
+
});
|
|
855
|
+
sub.unsubscribe();
|
|
856
|
+
resolveFirstShape!({
|
|
857
|
+
id: "late-acc",
|
|
858
|
+
balanceHistoryCache: createEmptyHistoryCache(),
|
|
859
|
+
});
|
|
860
|
+
await new Promise(r => setImmediate(r));
|
|
861
|
+
expect(completeSpy).not.toHaveBeenCalled();
|
|
862
|
+
});
|
|
493
863
|
});
|
|
494
864
|
|
|
495
865
|
describe("bip32asBuffer", () => {
|