@ledgerhq/live-common 34.53.0-nightly.20251122023607 → 34.53.0-nightly.20251125023817

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.
@@ -374,237 +374,259 @@ export const handlers = ({
374
374
  );
375
375
  }),
376
376
  "custom.exchange.swap": customWrapper<ExchangeSwapParams, SwapResult>(async params => {
377
- if (!params) {
378
- tracking.startExchangeNoParams(manifest);
379
- throw new ServerError(createUnknownError({ message: "params is undefined" }));
380
- }
377
+ try {
378
+ if (!params) {
379
+ tracking.startExchangeNoParams(manifest);
380
+ throw new ServerError(createUnknownError({ message: "params is undefined" }));
381
+ }
381
382
 
382
- const {
383
- provider,
384
- fromAmount,
385
- fromAmountAtomic,
386
- quoteId,
387
- toNewTokenId,
388
- customFeeConfig,
389
- swapAppVersion,
390
- sponsored,
391
- } = params;
392
-
393
- const trackingParams = {
394
- provider: params.provider,
395
- exchangeType: params.exchangeType,
396
- };
397
-
398
- tracking.startExchangeRequested(trackingParams);
399
-
400
- const exchangeStartParams: ExchangeStartParamsUiRequest = (await extractSwapStartParam(
401
- params,
402
- accounts,
403
- )) as SwapStartParamsUiRequest;
404
-
405
- const {
406
- fromCurrency,
407
- fromAccount,
408
- fromParentAccount,
409
- toCurrency,
410
- toAccount,
411
- toParentAccount,
412
- } = exchangeStartParams.exchange;
413
-
414
- if (!fromAccount || !fromCurrency) {
415
- throw new ServerError(createAccountNotFound(params.fromAccountId));
416
- }
383
+ const {
384
+ provider,
385
+ fromAmount,
386
+ fromAmountAtomic,
387
+ quoteId,
388
+ toNewTokenId,
389
+ customFeeConfig,
390
+ swapAppVersion,
391
+ sponsored,
392
+ } = params;
417
393
 
418
- const fromAccountAddress = fromParentAccount
419
- ? fromParentAccount.freshAddress
420
- : (fromAccount as Account).freshAddress;
421
-
422
- const toAccountAddress = toParentAccount
423
- ? toParentAccount.freshAddress
424
- : (toAccount as Account).freshAddress;
425
-
426
- // Step 1: Open the drawer and open exchange app
427
- const startExchange = async () => {
428
- return new Promise<{ transactionId: string; device?: ExchangeStartResult["device"] }>(
429
- (resolve, reject) => {
430
- uiExchangeStart({
431
- exchangeParams: exchangeStartParams,
432
- onSuccess: (nonce, device) => {
433
- tracking.startExchangeSuccess(trackingParams);
434
- resolve({ transactionId: nonce, device });
435
- },
436
- onCancel: error => {
437
- tracking.startExchangeFail(trackingParams);
438
- reject(error);
439
- },
440
- });
441
- },
442
- );
443
- };
444
-
445
- const { transactionId, device: deviceInfo } = await startExchange();
446
-
447
- const {
448
- binaryPayload,
449
- signature,
450
- payinAddress,
451
- swapId,
452
- payinExtraId,
453
- extraTransactionParameters,
454
- } = await retrieveSwapPayload({
455
- provider,
456
- deviceTransactionId: transactionId,
457
- fromAccountAddress,
458
- toAccountAddress,
459
- fromAccountCurrency: fromCurrency!.id,
460
- toAccountCurrency: toCurrency!.id,
461
- amount: fromAmount,
462
- amountInAtomicUnit: fromAmountAtomic,
463
- quoteId,
464
- toNewTokenId,
465
- }).catch((error: Error) => {
466
- throw error;
467
- });
468
-
469
- // Complete Swap
470
- const trackingCompleteParams = {
471
- provider: params.provider,
472
- exchangeType: params.exchangeType,
473
- };
474
- tracking.completeExchangeRequested(trackingCompleteParams);
475
-
476
- const strategyData = {
477
- recipient: payinAddress,
478
- amount: fromAmountAtomic,
479
- currency: fromCurrency as CryptoOrTokenCurrency,
480
- customFeeConfig: customFeeConfig ?? {},
481
- payinExtraId,
482
- extraTransactionParameters,
483
- sponsored,
484
- };
485
-
486
- const transaction: Transaction = await getStrategy(strategyData, "swap").catch(
487
- async error => {
394
+ const trackingParams = {
395
+ provider: params.provider,
396
+ exchangeType: params.exchangeType,
397
+ };
398
+
399
+ tracking.startExchangeRequested(trackingParams);
400
+
401
+ const exchangeStartParams: ExchangeStartParamsUiRequest = (await extractSwapStartParam(
402
+ params,
403
+ accounts,
404
+ )) as SwapStartParamsUiRequest;
405
+
406
+ const {
407
+ fromCurrency,
408
+ fromAccount,
409
+ fromParentAccount,
410
+ toCurrency,
411
+ toAccount,
412
+ toParentAccount,
413
+ } = exchangeStartParams.exchange;
414
+
415
+ if (!fromAccount || !fromCurrency) {
416
+ throw new ServerError(createAccountNotFound(params.fromAccountId));
417
+ }
418
+
419
+ const fromAccountAddress = fromParentAccount
420
+ ? fromParentAccount.freshAddress
421
+ : (fromAccount as Account).freshAddress;
422
+
423
+ const toAccountAddress = toParentAccount
424
+ ? toParentAccount.freshAddress
425
+ : (toAccount as Account).freshAddress;
426
+
427
+ // Step 1: Open the drawer and open exchange app
428
+ const startExchange = async () => {
429
+ return new Promise<{ transactionId: string; device?: ExchangeStartResult["device"] }>(
430
+ (resolve, reject) => {
431
+ uiExchangeStart({
432
+ exchangeParams: exchangeStartParams,
433
+ onSuccess: (nonce, device) => {
434
+ tracking.startExchangeSuccess(trackingParams);
435
+ resolve({ transactionId: nonce, device });
436
+ },
437
+ onCancel: error => {
438
+ tracking.startExchangeFail(trackingParams);
439
+ reject(error);
440
+ },
441
+ });
442
+ },
443
+ );
444
+ };
445
+
446
+ const { transactionId, device: deviceInfo } = await startExchange();
447
+
448
+ const {
449
+ binaryPayload,
450
+ signature,
451
+ payinAddress,
452
+ swapId,
453
+ payinExtraId,
454
+ extraTransactionParameters,
455
+ } = await retrieveSwapPayload({
456
+ provider,
457
+ deviceTransactionId: transactionId,
458
+ fromAccountAddress,
459
+ toAccountAddress,
460
+ fromAccountCurrency: fromCurrency!.id,
461
+ toAccountCurrency: toCurrency!.id,
462
+ amount: fromAmount,
463
+ amountInAtomicUnit: fromAmountAtomic,
464
+ quoteId,
465
+ toNewTokenId,
466
+ }).catch((error: Error) => {
488
467
  throw error;
489
- },
490
- );
468
+ });
491
469
 
492
- const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);
470
+ // Complete Swap
471
+ const trackingCompleteParams = {
472
+ provider: params.provider,
473
+ exchangeType: params.exchangeType,
474
+ };
475
+ tracking.completeExchangeRequested(trackingCompleteParams);
476
+
477
+ const strategyData = {
478
+ recipient: payinAddress,
479
+ amount: fromAmountAtomic,
480
+ currency: fromCurrency as CryptoOrTokenCurrency,
481
+ customFeeConfig: customFeeConfig ?? {},
482
+ payinExtraId,
483
+ extraTransactionParameters,
484
+ sponsored,
485
+ };
493
486
 
494
- if (transaction.family !== mainFromAccount.currency.family) {
495
- return Promise.reject(
496
- new Error(
497
- `Account and transaction must be from the same family. Account family: ${mainFromAccount.currency.family}, Transaction family: ${transaction.family}`,
498
- ),
487
+ const transaction: Transaction = await getStrategy(strategyData, "swap").catch(
488
+ async error => {
489
+ throw error;
490
+ },
499
491
  );
500
- }
501
492
 
502
- const accountBridge = getAccountBridge(fromAccount, fromParentAccount);
503
-
504
- /**
505
- * 'subAccountId' is used for ETH and it's ERC-20 tokens.
506
- * This field is ignored for BTC
507
- */
508
- const subAccountId =
509
- fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined;
510
-
511
- const bridgeTx = accountBridge.createTransaction(fromAccount);
512
- /**
513
- * We append the `recipient` to the tx created from `createTransaction`
514
- * to avoid having userGasLimit reset to null for ETH txs
515
- * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts
516
- */
517
- const tx = accountBridge.updateTransaction(
518
- {
519
- ...bridgeTx,
520
- recipient: transaction.recipient,
521
- },
522
- {
523
- ...transaction,
524
- feesStrategy: params.feeStrategy.toLowerCase(),
525
- subAccountId,
526
- },
527
- );
493
+ const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);
528
494
 
529
- // Get amountExpectedTo and magnitudeAwareRate from binary payload
530
- const decodePayload = await decodeSwapPayload(binaryPayload);
531
- const amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString());
532
- const magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount);
533
- const refundAddress = decodePayload.refundAddress;
534
- const payoutAddress = decodePayload.payoutAddress;
495
+ if (transaction.family !== mainFromAccount.currency.family) {
496
+ return Promise.reject(
497
+ new Error(
498
+ `Account and transaction must be from the same family. Account family: ${mainFromAccount.currency.family}, Transaction family: ${transaction.family}`,
499
+ ),
500
+ );
501
+ }
535
502
 
536
- // tx.amount should be BigNumber
537
- tx.amount = new BigNumber(tx.amount);
503
+ const accountBridge = getAccountBridge(fromAccount, fromParentAccount);
538
504
 
539
- return new Promise((resolve, reject) =>
540
- uiSwap({
541
- exchangeParams: {
542
- exchangeType: ExchangeType.SWAP,
543
- provider: params.provider,
544
- transaction: tx,
545
- signature: signature,
546
- binaryPayload: binaryPayload,
547
- exchange: {
548
- fromAccount,
549
- fromParentAccount,
550
- toAccount,
551
- toParentAccount,
552
- fromCurrency: fromCurrency!,
553
- toCurrency: toCurrency!,
554
- },
555
- feesStrategy: params.feeStrategy,
556
- swapId: swapId,
557
- amountExpectedTo: amountExpectedTo.toNumber(),
558
- magnitudeAwareRate,
559
- refundAddress,
560
- payoutAddress,
561
- sponsored,
505
+ /**
506
+ * 'subAccountId' is used for ETH and it's ERC-20 tokens.
507
+ * This field is ignored for BTC
508
+ */
509
+ const subAccountId =
510
+ fromParentAccount && fromParentAccount.id !== fromAccount.id ? fromAccount.id : undefined;
511
+
512
+ const bridgeTx = accountBridge.createTransaction(fromAccount);
513
+ /**
514
+ * We append the `recipient` to the tx created from `createTransaction`
515
+ * to avoid having userGasLimit reset to null for ETH txs
516
+ * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts
517
+ */
518
+ const tx = accountBridge.updateTransaction(
519
+ {
520
+ ...bridgeTx,
521
+ recipient: transaction.recipient,
562
522
  },
563
- onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => {
564
- tracking.completeExchangeSuccess({
565
- ...trackingParams,
566
- currency: transaction.family,
567
- });
568
-
569
- setBroadcastTransaction({
570
- provider,
571
- result: { operation: operationHash, swapId },
572
- sourceCurrencyId: fromCurrency.id,
573
- targetCurrencyId: toCurrency?.id,
574
- hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
575
- swapAppVersion,
576
- fromAccountAddress,
577
- toAccountAddress,
578
- fromAmount,
579
- });
580
-
581
- resolve({ operationHash, swapId });
523
+ {
524
+ ...transaction,
525
+ feesStrategy: params.feeStrategy.toLowerCase(),
526
+ subAccountId,
582
527
  },
583
- onCancel: error => {
584
- postSwapCancelled({
585
- provider: provider,
528
+ );
529
+
530
+ // Get amountExpectedTo and magnitudeAwareRate from binary payload
531
+ const decodePayload = await decodeSwapPayload(binaryPayload);
532
+ const amountExpectedTo = new BigNumber(decodePayload.amountToWallet.toString());
533
+ const magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount);
534
+ const refundAddress = decodePayload.refundAddress;
535
+ const payoutAddress = decodePayload.payoutAddress;
536
+
537
+ // tx.amount should be BigNumber
538
+ tx.amount = new BigNumber(tx.amount);
539
+
540
+ return new Promise((resolve, reject) =>
541
+ uiSwap({
542
+ exchangeParams: {
543
+ exchangeType: ExchangeType.SWAP,
544
+ provider: params.provider,
545
+ transaction: tx,
546
+ signature: signature,
547
+ binaryPayload: binaryPayload,
548
+ exchange: {
549
+ fromAccount,
550
+ fromParentAccount,
551
+ toAccount,
552
+ toParentAccount,
553
+ fromCurrency: fromCurrency!,
554
+ toCurrency: toCurrency!,
555
+ },
556
+ feesStrategy: params.feeStrategy,
586
557
  swapId: swapId,
587
- swapStep: getSwapStepFromError(error),
588
- statusCode: error.name,
589
- errorMessage: error.message,
590
- sourceCurrencyId: fromCurrency.id,
591
- targetCurrencyId: toCurrency?.id,
592
- hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
593
- swapType: quoteId ? "fixed" : "float",
594
- swapAppVersion,
595
- fromAccountAddress,
596
- toAccountAddress,
558
+ amountExpectedTo: amountExpectedTo.toNumber(),
559
+ magnitudeAwareRate,
597
560
  refundAddress,
598
561
  payoutAddress,
599
- fromAmount,
600
- seedIdFrom: mainFromAccount.seedIdentifier,
601
- seedIdTo: toParentAccount?.seedIdentifier || (toAccount as Account)?.seedIdentifier,
602
- });
562
+ sponsored,
563
+ },
564
+ onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => {
565
+ tracking.completeExchangeSuccess({
566
+ ...trackingParams,
567
+ currency: transaction.family,
568
+ });
603
569
 
604
- reject(error);
605
- },
606
- }),
607
- );
570
+ setBroadcastTransaction({
571
+ provider,
572
+ result: { operation: operationHash, swapId },
573
+ sourceCurrencyId: fromCurrency.id,
574
+ targetCurrencyId: toCurrency?.id,
575
+ hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
576
+ swapAppVersion,
577
+ fromAccountAddress,
578
+ toAccountAddress,
579
+ fromAmount,
580
+ });
581
+
582
+ resolve({ operationHash, swapId });
583
+ },
584
+ onCancel: error => {
585
+ postSwapCancelled({
586
+ provider: provider,
587
+ swapId: swapId,
588
+ swapStep: getSwapStepFromError(error),
589
+ statusCode: error.name,
590
+ errorMessage: error.message,
591
+ sourceCurrencyId: fromCurrency.id,
592
+ targetCurrencyId: toCurrency?.id,
593
+ hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
594
+ swapType: quoteId ? "fixed" : "float",
595
+ swapAppVersion,
596
+ fromAccountAddress,
597
+ toAccountAddress,
598
+ refundAddress,
599
+ payoutAddress,
600
+ fromAmount,
601
+ seedIdFrom: mainFromAccount.seedIdentifier,
602
+ seedIdTo: toParentAccount?.seedIdentifier || (toAccount as Account)?.seedIdentifier,
603
+ });
604
+
605
+ reject(error);
606
+ },
607
+ }),
608
+ );
609
+ } catch (error) {
610
+ // Skip DrawerClosedError
611
+ if (isDrawerClosedError(error)) {
612
+ throw error;
613
+ }
614
+ // Global catch for any errors during the swap process
615
+ // Convert the error to SwapLiveError format for WebviewErrorDrawer
616
+ const swapError = getSwapError(error);
617
+
618
+ return new Promise((resolve, reject) => {
619
+ uiError({
620
+ error: swapError,
621
+ onSuccess: () => {
622
+ reject(error);
623
+ },
624
+ onCancel: () => {
625
+ reject(error);
626
+ },
627
+ });
628
+ });
629
+ }
608
630
  }),
609
631
 
610
632
  "custom.isReady": customWrapper<void, void>(async () => {
@@ -844,3 +866,55 @@ async function getStrategy(
844
866
  );
845
867
  }
846
868
  }
869
+
870
+ function isDrawerClosedError(error: unknown) {
871
+ return (
872
+ error && typeof error === "object" && "name" in error && error.name === "DrawerClosedError"
873
+ );
874
+ }
875
+
876
+ function getSwapError(error: unknown) {
877
+ let swapError: SwapLiveError;
878
+ if (typeof error === "object" && error !== null) {
879
+ const err = error as any;
880
+
881
+ // Handle Axios error format: error.response.data.error
882
+ const apiError = err.response?.data?.error || err.error;
883
+ const errorMessage =
884
+ apiError?.message || err.message || (err instanceof Error ? err.message : String(error));
885
+ const messageKey = apiError?.messageKey || err.messageKey;
886
+
887
+ // Create a plain object with only serializable properties to avoid analytics issues
888
+ // Preserve the original error structure for WebviewErrorDrawer to parse
889
+ // It checks error.cause.response.data.error.messageKey (line 96)
890
+ swapError = Object.assign(Object.create(null), {
891
+ message: err.message,
892
+ name: err.name,
893
+ code: err.code,
894
+ status: err.status,
895
+ swap: err.swap,
896
+ cause: {
897
+ message: errorMessage,
898
+ swapCode: apiError?.code,
899
+ response: {
900
+ data: {
901
+ error: {
902
+ messageKey: messageKey,
903
+ message: errorMessage,
904
+ },
905
+ },
906
+ },
907
+ },
908
+ }) as SwapLiveError;
909
+
910
+ // Add hasOwnProperty method to the plain object
911
+ Object.setPrototypeOf(swapError, Object.prototype);
912
+ } else {
913
+ swapError = {
914
+ cause: {
915
+ message: error instanceof Error ? error.message : String(error),
916
+ },
917
+ };
918
+ }
919
+ return swapError;
920
+ }