@ledgerhq/live-common 34.40.0-nightly.1 → 34.40.0-nightly.2

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 (66) hide show
  1. package/lib/exchange/error.d.ts +1 -0
  2. package/lib/exchange/error.d.ts.map +1 -1
  3. package/lib/exchange/error.js +11 -1
  4. package/lib/exchange/error.js.map +1 -1
  5. package/lib/exchange/swap/api/v5/actions.d.ts +3 -0
  6. package/lib/exchange/swap/api/v5/actions.d.ts.map +1 -0
  7. package/lib/exchange/swap/api/v5/actions.js +36 -0
  8. package/lib/exchange/swap/api/v5/actions.js.map +1 -0
  9. package/lib/exchange/swap/getCompleteSwapHistory.js +1 -1
  10. package/lib/exchange/swap/getCompleteSwapHistory.js.map +1 -1
  11. package/lib/exchange/swap/postSwapState.js +3 -3
  12. package/lib/exchange/swap/postSwapState.js.map +1 -1
  13. package/lib/exchange/swap/setBroadcastTransaction.d.ts +1 -1
  14. package/lib/exchange/swap/setBroadcastTransaction.d.ts.map +1 -1
  15. package/lib/exchange/swap/setBroadcastTransaction.js +1 -1
  16. package/lib/exchange/swap/setBroadcastTransaction.js.map +1 -1
  17. package/lib/exchange/swap/transactionStrategies.d.ts +25 -0
  18. package/lib/exchange/swap/transactionStrategies.d.ts.map +1 -0
  19. package/lib/exchange/swap/transactionStrategies.js +152 -0
  20. package/lib/exchange/swap/transactionStrategies.js.map +1 -0
  21. package/lib/exchange/swap/types.d.ts +58 -0
  22. package/lib/exchange/swap/types.d.ts.map +1 -1
  23. package/lib/wallet-api/Exchange/server.d.ts +29 -10
  24. package/lib/wallet-api/Exchange/server.d.ts.map +1 -1
  25. package/lib/wallet-api/Exchange/server.js +202 -7
  26. package/lib/wallet-api/Exchange/server.js.map +1 -1
  27. package/lib/wallet-api/Exchange/server.test.js +3 -1
  28. package/lib/wallet-api/Exchange/server.test.js.map +1 -1
  29. package/lib-es/exchange/error.d.ts +1 -0
  30. package/lib-es/exchange/error.d.ts.map +1 -1
  31. package/lib-es/exchange/error.js +9 -0
  32. package/lib-es/exchange/error.js.map +1 -1
  33. package/lib-es/exchange/swap/api/v5/actions.d.ts +3 -0
  34. package/lib-es/exchange/swap/api/v5/actions.d.ts.map +1 -0
  35. package/lib-es/exchange/swap/api/v5/actions.js +29 -0
  36. package/lib-es/exchange/swap/api/v5/actions.js.map +1 -0
  37. package/lib-es/exchange/swap/getCompleteSwapHistory.js +1 -1
  38. package/lib-es/exchange/swap/getCompleteSwapHistory.js.map +1 -1
  39. package/lib-es/exchange/swap/postSwapState.js +1 -1
  40. package/lib-es/exchange/swap/postSwapState.js.map +1 -1
  41. package/lib-es/exchange/swap/setBroadcastTransaction.d.ts +1 -1
  42. package/lib-es/exchange/swap/setBroadcastTransaction.d.ts.map +1 -1
  43. package/lib-es/exchange/swap/setBroadcastTransaction.js +1 -1
  44. package/lib-es/exchange/swap/setBroadcastTransaction.js.map +1 -1
  45. package/lib-es/exchange/swap/transactionStrategies.d.ts +25 -0
  46. package/lib-es/exchange/swap/transactionStrategies.d.ts.map +1 -0
  47. package/lib-es/exchange/swap/transactionStrategies.js +140 -0
  48. package/lib-es/exchange/swap/transactionStrategies.js.map +1 -0
  49. package/lib-es/exchange/swap/types.d.ts +58 -0
  50. package/lib-es/exchange/swap/types.d.ts.map +1 -1
  51. package/lib-es/wallet-api/Exchange/server.d.ts +29 -10
  52. package/lib-es/wallet-api/Exchange/server.d.ts.map +1 -1
  53. package/lib-es/wallet-api/Exchange/server.js +204 -9
  54. package/lib-es/wallet-api/Exchange/server.js.map +1 -1
  55. package/lib-es/wallet-api/Exchange/server.test.js +3 -1
  56. package/lib-es/wallet-api/Exchange/server.test.js.map +1 -1
  57. package/package.json +4 -4
  58. package/src/exchange/error.ts +10 -0
  59. package/src/exchange/swap/api/v5/actions.ts +36 -0
  60. package/src/exchange/swap/getCompleteSwapHistory.ts +1 -1
  61. package/src/exchange/swap/postSwapState.ts +1 -1
  62. package/src/exchange/swap/setBroadcastTransaction.ts +2 -2
  63. package/src/exchange/swap/transactionStrategies.ts +234 -0
  64. package/src/exchange/swap/types.ts +63 -0
  65. package/src/wallet-api/Exchange/server.test.ts +7 -5
  66. package/src/wallet-api/Exchange/server.ts +335 -32
@@ -1,48 +1,58 @@
1
1
  /* eslint-disable no-console */
2
- import { RPCHandler, customWrapper } from "@ledgerhq/wallet-api-server";
2
+ import {
3
+ getMainAccount,
4
+ getParentAccount,
5
+ makeEmptyTokenAccount,
6
+ } from "@ledgerhq/coin-framework/account/index";
7
+ import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets";
8
+ import { decodeSwapPayload } from "@ledgerhq/hw-app-exchange";
9
+ import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
10
+ import { Account, AccountLike, getCurrencyForAccount, TokenAccount } from "@ledgerhq/types-live";
3
11
  import {
4
12
  createAccountNotFound,
5
13
  createCurrencyNotFound,
14
+ createUnknownError,
6
15
  deserializeTransaction,
7
16
  ServerError,
8
17
  } from "@ledgerhq/wallet-api-core";
9
- import {
10
- getParentAccount,
11
- getMainAccount,
12
- makeEmptyTokenAccount,
13
- } from "@ledgerhq/coin-framework/account/index";
14
- import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
15
- import { AccountLike, getCurrencyForAccount, TokenAccount } from "@ledgerhq/types-live";
16
- import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets";
17
18
  import {
18
19
  ExchangeCompleteParams,
19
20
  ExchangeCompleteResult,
20
21
  ExchangeStartParams,
21
- ExchangeStartSwapParams,
22
22
  ExchangeStartResult,
23
- ExchangeType,
24
23
  ExchangeStartSellParams,
24
+ ExchangeStartSwapParams,
25
+ ExchangeSwapParams,
26
+ ExchangeType,
25
27
  SwapLiveError,
28
+ SwapResult,
26
29
  } from "@ledgerhq/wallet-api-exchange-module";
27
- import { decodeSwapPayload } from "@ledgerhq/hw-app-exchange";
28
- import { TrackingAPI } from "./tracking";
29
- import { AppManifest } from "../types";
30
+ import { customWrapper, RPCHandler } from "@ledgerhq/wallet-api-server";
31
+ import { BigNumber } from "bignumber.js";
32
+ import { getAccountBridge } from "../../bridge";
33
+ import { retrieveSwapPayload } from "../../exchange/swap/api/v5/actions";
34
+ import { transactionStrategy } from "../../exchange/swap/transactionStrategies";
35
+ import { ExchangeSwap } from "../../exchange/swap/types";
36
+ import { Exchange } from "../../exchange/types";
37
+ import { Transaction } from "../../generated/types";
30
38
  import {
31
39
  getAccountIdFromWalletAccountId,
32
40
  getWalletAPITransactionSignFlowInfos,
33
41
  } from "../converters";
34
- import { getAccountBridge } from "../../bridge";
35
- import { Exchange } from "../../exchange/types";
36
- import { Transaction } from "../../generated/types";
42
+ import { AppManifest } from "../types";
37
43
  import {
38
- ExchangeError,
39
44
  createAccounIdNotFound,
40
45
  createWrongSellParams,
41
46
  createWrongSwapParams,
47
+ ExchangeError,
42
48
  } from "./error";
49
+ import { TrackingAPI } from "./tracking";
50
+ import { getSwapStepFromError } from "../../exchange/error";
51
+ import { postSwapCancelled } from "../../exchange/swap";
52
+ import { DeviceModelId } from "@ledgerhq/types-devices";
53
+ import { setBroadcastTransaction } from "../../exchange/swap/setBroadcastTransaction";
43
54
 
44
55
  export { ExchangeType };
45
- import { BigNumber } from "bignumber.js";
46
56
 
47
57
  type Handlers = {
48
58
  "custom.exchange.start": RPCHandler<
@@ -52,6 +62,7 @@ type Handlers = {
52
62
  "custom.exchange.complete": RPCHandler<ExchangeCompleteResult, ExchangeCompleteParams>;
53
63
  "custom.exchange.error": RPCHandler<void, SwapLiveError>;
54
64
  "custom.isReady": RPCHandler<void, void>;
65
+ "custom.exchange.swap": RPCHandler<SwapResult, ExchangeSwapParams>;
55
66
  };
56
67
 
57
68
  export type CompleteExchangeUiRequest = {
@@ -68,21 +79,33 @@ export type CompleteExchangeUiRequest = {
68
79
  refundAddress?: string;
69
80
  payoutAddress?: string;
70
81
  };
82
+ type FundStartParamsUiRequest = {
83
+ exchangeType: "FUND";
84
+ };
85
+
86
+ type SellStartParamsUiRequest = {
87
+ exchangeType: "SELL";
88
+ provider: string;
89
+ exchange: Partial<Exchange> | undefined;
90
+ };
91
+
92
+ type SwapStartParamsUiRequest = {
93
+ exchangeType: "SWAP";
94
+ provider: string;
95
+ exchange: Partial<ExchangeSwap>;
96
+ };
71
97
 
72
98
  type ExchangeStartParamsUiRequest =
73
- | {
74
- exchangeType: "FUND";
75
- }
76
- | {
77
- exchangeType: "SELL";
78
- provider: string;
79
- exchange: Partial<Exchange> | undefined;
80
- }
81
- | {
82
- exchangeType: "SWAP";
83
- provider: string;
84
- exchange: Partial<Exchange>;
85
- };
99
+ | FundStartParamsUiRequest
100
+ | SellStartParamsUiRequest
101
+ | SwapStartParamsUiRequest;
102
+
103
+ export type SwapUiRequest = CompleteExchangeUiRequest & {
104
+ provider?: string;
105
+ fromAccountId?: string;
106
+ toAccountId?: string;
107
+ tokenCurrency?: string;
108
+ };
86
109
 
87
110
  type ExchangeUiHooks = {
88
111
  "custom.exchange.start": (params: {
@@ -101,6 +124,11 @@ type ExchangeUiHooks = {
101
124
  onCancel: () => void;
102
125
  }) => void;
103
126
  "custom.isReady": (params: { onSuccess: () => void; onCancel: () => void }) => void;
127
+ "custom.exchange.swap": (params: {
128
+ exchangeParams: SwapUiRequest;
129
+ onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => void;
130
+ onCancel: (error: Error) => void;
131
+ }) => void;
104
132
  };
105
133
 
106
134
  export const handlers = ({
@@ -112,6 +140,7 @@ export const handlers = ({
112
140
  "custom.exchange.complete": uiExchangeComplete,
113
141
  "custom.exchange.error": uiError,
114
142
  "custom.isReady": uiIsReady,
143
+ "custom.exchange.swap": uiSwap,
115
144
  },
116
145
  }: {
117
146
  accounts: AccountLike[];
@@ -343,6 +372,219 @@ export const handlers = ({
343
372
  }),
344
373
  );
345
374
  }),
375
+ "custom.exchange.swap": customWrapper<ExchangeSwapParams, SwapResult>(async params => {
376
+ if (!params) {
377
+ tracking.startExchangeNoParams(manifest);
378
+ throw new ServerError(createUnknownError({ message: "params is undefined" }));
379
+ }
380
+
381
+ const { provider, fromAmount, fromAmountAtomic, quoteId, toNewTokenId, customFeeConfig } =
382
+ params;
383
+
384
+ const trackingParams = {
385
+ provider: params.provider,
386
+ exchangeType: params.exchangeType,
387
+ };
388
+
389
+ tracking.startExchangeRequested(trackingParams);
390
+
391
+ const exchangeStartParams: ExchangeStartParamsUiRequest = extractSwapStartParam(
392
+ params,
393
+ accounts,
394
+ ) as SwapStartParamsUiRequest;
395
+
396
+ const {
397
+ fromCurrency,
398
+ fromAccount,
399
+ fromParentAccount,
400
+ toCurrency,
401
+ toAccount,
402
+ toParentAccount,
403
+ } = exchangeStartParams.exchange;
404
+
405
+ if (!fromAccount || !fromCurrency) {
406
+ throw new ServerError(createAccountNotFound(params.fromAccountId));
407
+ }
408
+
409
+ const fromAccountAddress = fromParentAccount
410
+ ? fromParentAccount.freshAddress
411
+ : (fromAccount as Account).freshAddress;
412
+
413
+ const toAccountAddress = toParentAccount
414
+ ? toParentAccount.freshAddress
415
+ : (toAccount as Account).freshAddress;
416
+
417
+ // Step 1: Open the drawer and open exchange app
418
+ const startExchange = async () => {
419
+ console.log("LLD custom.exchange.swap uiExchangeStart", exchangeStartParams);
420
+
421
+ return new Promise<{ transactionId: string; device?: ExchangeStartResult["device"] }>(
422
+ (resolve, reject) => {
423
+ uiExchangeStart({
424
+ exchangeParams: exchangeStartParams,
425
+ onSuccess: (nonce, device) => {
426
+ tracking.startExchangeSuccess(trackingParams);
427
+ resolve({ transactionId: nonce, device });
428
+ },
429
+ onCancel: error => {
430
+ tracking.startExchangeFail(trackingParams);
431
+ reject(error);
432
+ },
433
+ });
434
+ },
435
+ );
436
+ };
437
+
438
+ const { transactionId, device: deviceInfo } = await startExchange();
439
+
440
+ const {
441
+ binaryPayload,
442
+ signature,
443
+ payinAddress,
444
+ swapId,
445
+ payinExtraId,
446
+ extraTransactionParameters,
447
+ } = await retrieveSwapPayload({
448
+ provider,
449
+ deviceTransactionId: transactionId,
450
+ fromAccountAddress,
451
+ toAccountAddress,
452
+ fromAccountCurrency: fromCurrency!.id,
453
+ toAccountCurrency: toCurrency!.id,
454
+ amount: fromAmount,
455
+ amountInAtomicUnit: fromAmountAtomic,
456
+ quoteId,
457
+ toNewTokenId,
458
+ }).catch((error: Error) => {
459
+ throw error;
460
+ });
461
+
462
+ // Complete Swap
463
+ const trackingCompleteParams = {
464
+ provider: params.provider,
465
+ exchangeType: params.exchangeType,
466
+ };
467
+ tracking.completeExchangeRequested(trackingCompleteParams);
468
+
469
+ const strategyData = {
470
+ recipient: payinAddress,
471
+ amount: fromAmountAtomic,
472
+ currency: fromCurrency as CryptoOrTokenCurrency,
473
+ customFeeConfig: customFeeConfig ?? {},
474
+ payinExtraId,
475
+ extraTransactionParameters,
476
+ };
477
+
478
+ const transaction = await getStrategy(strategyData, "swap").catch(async error => {
479
+ throw error;
480
+ });
481
+
482
+ const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);
483
+ const mainFromAccountFamily = mainFromAccount.currency.family;
484
+
485
+ if (transaction.family !== mainFromAccountFamily) {
486
+ return Promise.reject(
487
+ new Error(
488
+ `Account and transaction must be from the same family. Account family: ${mainFromAccountFamily}, Transaction family: ${transaction.family}`,
489
+ ),
490
+ );
491
+ }
492
+
493
+ const accountBridge = getAccountBridge(fromAccount, fromParentAccount);
494
+
495
+ /**
496
+ * 'subAccountId' is used for ETH and it's ERC-20 tokens.
497
+ * This field is ignored for BTC
498
+ */
499
+ const subAccountId =
500
+ fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined;
501
+
502
+ const bridgeTx = accountBridge.createTransaction(fromAccount);
503
+ /**
504
+ * We append the `recipient` to the tx created from `createTransaction`
505
+ * to avoid having userGasLimit reset to null for ETH txs
506
+ * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts
507
+ */
508
+ const tx = accountBridge.updateTransaction(
509
+ {
510
+ ...bridgeTx,
511
+ recipient: transaction.recipient,
512
+ },
513
+ {
514
+ ...transaction,
515
+ feesStrategy: params.feeStrategy.toLowerCase(),
516
+ subAccountId,
517
+ },
518
+ );
519
+
520
+ // Get amountExpectedTo and magnitudeAwareRate from binary payload
521
+ const decodePayload = await decodeSwapPayload(binaryPayload);
522
+ const amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString());
523
+ const magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount);
524
+ const refundAddress = decodePayload.refundAddress;
525
+ const payoutAddress = decodePayload.payoutAddress;
526
+
527
+ // tx.amount should be BigNumber
528
+ tx.amount = new BigNumber(tx.amount);
529
+
530
+ return new Promise((resolve, reject) =>
531
+ uiSwap({
532
+ exchangeParams: {
533
+ exchangeType: ExchangeType.SWAP,
534
+ provider: params.provider,
535
+ transaction: tx,
536
+ signature: signature,
537
+ binaryPayload: binaryPayload,
538
+ exchange: {
539
+ fromAccount,
540
+ fromParentAccount,
541
+ toAccount,
542
+ toParentAccount,
543
+ fromCurrency: fromCurrency!,
544
+ toCurrency: toCurrency!,
545
+ },
546
+ feesStrategy: params.feeStrategy,
547
+ swapId: swapId,
548
+ amountExpectedTo: amountExpectedTo.toNumber(),
549
+ magnitudeAwareRate,
550
+ refundAddress,
551
+ payoutAddress,
552
+ },
553
+ onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => {
554
+ tracking.completeExchangeSuccess({
555
+ ...trackingParams,
556
+ currency: transaction.family,
557
+ });
558
+
559
+ setBroadcastTransaction({
560
+ provider,
561
+ result: { operation: operationHash, swapId },
562
+ sourceCurrencyId: fromCurrency.id,
563
+ targetCurrencyId: toCurrency?.id,
564
+ hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
565
+ });
566
+
567
+ resolve({ operationHash, swapId });
568
+ },
569
+ onCancel: error => {
570
+ postSwapCancelled({
571
+ provider: provider,
572
+ swapId: swapId,
573
+ swapStep: getSwapStepFromError(error),
574
+ statusCode: error.name,
575
+ errorMessage: error.message,
576
+ sourceCurrencyId: fromCurrency.id,
577
+ targetCurrencyId: toCurrency?.id,
578
+ hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
579
+ swapType: quoteId ? "fixed" : "float",
580
+ });
581
+
582
+ reject(error);
583
+ },
584
+ }),
585
+ );
586
+ }),
587
+
346
588
  "custom.isReady": customWrapper<void, void>(async () => {
347
589
  return new Promise((resolve, reject) =>
348
590
  uiIsReady({
@@ -402,8 +644,10 @@ function extractSwapStartParam(
402
644
  exchange: {
403
645
  fromAccount,
404
646
  fromParentAccount,
647
+ fromCurrency: getCurrencyForAccount(fromAccount),
405
648
  toAccount: newTokenAccount ? newTokenAccount : toAccount,
406
649
  toParentAccount: newTokenAccount ? toAccount : toParentAccount,
650
+ toCurrency: getCurrencyForAccount(toAccount),
407
651
  },
408
652
  };
409
653
  }
@@ -468,3 +712,62 @@ async function getToCurrency(
468
712
 
469
713
  return newTokenAccount?.token ?? getCurrencyForAccount(toAccount);
470
714
  }
715
+
716
+ interface StrategyParams {
717
+ recipient: string;
718
+ amount: BigNumber | number | string;
719
+ currency: CryptoOrTokenCurrency;
720
+ customFeeConfig?: Record<string, unknown>;
721
+ payinExtraId?: string;
722
+ extraTransactionParameters?: string;
723
+ }
724
+
725
+ async function getStrategy(
726
+ {
727
+ recipient,
728
+ amount,
729
+ currency,
730
+ customFeeConfig,
731
+ payinExtraId,
732
+ extraTransactionParameters,
733
+ }: StrategyParams,
734
+ customErrorType?: any,
735
+ ): Promise<Transaction> {
736
+ const family =
737
+ currency.type === "TokenCurrency"
738
+ ? (currency.parentCurrency?.family as Transaction["family"])
739
+ : (currency.family as Transaction["family"]);
740
+
741
+ if (!family) {
742
+ throw new Error(`TokenCurrency missing parentCurrency family: ${currency.id}`);
743
+ }
744
+
745
+ // Remove unsupported utxoStrategy for now
746
+ if (customFeeConfig?.utxoStrategy) {
747
+ delete customFeeConfig.utxoStrategy;
748
+ }
749
+
750
+ // Normalize family key for strategy lookup
751
+ const familyKey = family === "evm" ? "ethereum" : family;
752
+ const strategy = transactionStrategy?.[familyKey];
753
+
754
+ if (!strategy) {
755
+ throw new Error(`No transaction strategy found for family: ${familyKey}`);
756
+ }
757
+
758
+ try {
759
+ return await strategy({
760
+ family,
761
+ amount,
762
+ recipient,
763
+ customFeeConfig: customFeeConfig || {},
764
+ payinExtraId,
765
+ extraTransactionParameters,
766
+ customErrorType,
767
+ });
768
+ } catch (error) {
769
+ throw new Error(
770
+ `Failed to execute transaction strategy for family: ${familyKey}. Reason: ${(error as Error).message}`,
771
+ );
772
+ }
773
+ }