@ledgerhq/coin-canton 0.12.0-nightly.20251211024123 → 0.12.0-nightly.20251213023821

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 (94) hide show
  1. package/CHANGELOG.md +15 -9
  2. package/lib/bridge/buildSubAccounts.d.ts +23 -0
  3. package/lib/bridge/buildSubAccounts.d.ts.map +1 -0
  4. package/lib/bridge/buildSubAccounts.js +80 -0
  5. package/lib/bridge/buildSubAccounts.js.map +1 -0
  6. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  7. package/lib/bridge/getTransactionStatus.js +45 -24
  8. package/lib/bridge/getTransactionStatus.js.map +1 -1
  9. package/lib/bridge/index.d.ts.map +1 -1
  10. package/lib/bridge/index.js +2 -0
  11. package/lib/bridge/index.js.map +1 -1
  12. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  13. package/lib/bridge/prepareTransaction.js +19 -3
  14. package/lib/bridge/prepareTransaction.js.map +1 -1
  15. package/lib/bridge/signOperation.d.ts.map +1 -1
  16. package/lib/bridge/signOperation.js +3 -0
  17. package/lib/bridge/signOperation.js.map +1 -1
  18. package/lib/bridge/sync.d.ts +3 -0
  19. package/lib/bridge/sync.d.ts.map +1 -1
  20. package/lib/bridge/sync.js +115 -19
  21. package/lib/bridge/sync.js.map +1 -1
  22. package/lib/bridge/validateAddress.d.ts +3 -0
  23. package/lib/bridge/validateAddress.d.ts.map +1 -0
  24. package/lib/bridge/validateAddress.js +8 -0
  25. package/lib/bridge/validateAddress.js.map +1 -0
  26. package/lib/common-logic/account/getBalance.d.ts +1 -0
  27. package/lib/common-logic/account/getBalance.d.ts.map +1 -1
  28. package/lib/common-logic/account/getBalance.js +1 -0
  29. package/lib/common-logic/account/getBalance.js.map +1 -1
  30. package/lib/common-logic/transaction/craftTransaction.d.ts +1 -0
  31. package/lib/common-logic/transaction/craftTransaction.d.ts.map +1 -1
  32. package/lib/common-logic/transaction/craftTransaction.js +3 -0
  33. package/lib/common-logic/transaction/craftTransaction.js.map +1 -1
  34. package/lib/network/gateway.d.ts +30 -12
  35. package/lib/network/gateway.d.ts.map +1 -1
  36. package/lib/network/gateway.js +44 -1
  37. package/lib/network/gateway.js.map +1 -1
  38. package/lib/types/bridge.d.ts +2 -0
  39. package/lib/types/bridge.d.ts.map +1 -1
  40. package/lib-es/bridge/buildSubAccounts.d.ts +23 -0
  41. package/lib-es/bridge/buildSubAccounts.d.ts.map +1 -0
  42. package/lib-es/bridge/buildSubAccounts.js +74 -0
  43. package/lib-es/bridge/buildSubAccounts.js.map +1 -0
  44. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  45. package/lib-es/bridge/getTransactionStatus.js +46 -25
  46. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  47. package/lib-es/bridge/index.d.ts.map +1 -1
  48. package/lib-es/bridge/index.js +2 -0
  49. package/lib-es/bridge/index.js.map +1 -1
  50. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  51. package/lib-es/bridge/prepareTransaction.js +19 -3
  52. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  53. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  54. package/lib-es/bridge/signOperation.js +3 -0
  55. package/lib-es/bridge/signOperation.js.map +1 -1
  56. package/lib-es/bridge/sync.d.ts +3 -0
  57. package/lib-es/bridge/sync.d.ts.map +1 -1
  58. package/lib-es/bridge/sync.js +115 -20
  59. package/lib-es/bridge/sync.js.map +1 -1
  60. package/lib-es/bridge/validateAddress.d.ts +3 -0
  61. package/lib-es/bridge/validateAddress.d.ts.map +1 -0
  62. package/lib-es/bridge/validateAddress.js +5 -0
  63. package/lib-es/bridge/validateAddress.js.map +1 -0
  64. package/lib-es/common-logic/account/getBalance.d.ts +1 -0
  65. package/lib-es/common-logic/account/getBalance.d.ts.map +1 -1
  66. package/lib-es/common-logic/account/getBalance.js +1 -0
  67. package/lib-es/common-logic/account/getBalance.js.map +1 -1
  68. package/lib-es/common-logic/transaction/craftTransaction.d.ts +1 -0
  69. package/lib-es/common-logic/transaction/craftTransaction.d.ts.map +1 -1
  70. package/lib-es/common-logic/transaction/craftTransaction.js +3 -0
  71. package/lib-es/common-logic/transaction/craftTransaction.js.map +1 -1
  72. package/lib-es/network/gateway.d.ts +30 -12
  73. package/lib-es/network/gateway.d.ts.map +1 -1
  74. package/lib-es/network/gateway.js +41 -0
  75. package/lib-es/network/gateway.js.map +1 -1
  76. package/lib-es/types/bridge.d.ts +2 -0
  77. package/lib-es/types/bridge.d.ts.map +1 -1
  78. package/package.json +9 -9
  79. package/src/bridge/buildSubAccounts.test.ts +120 -0
  80. package/src/bridge/buildSubAccounts.ts +132 -0
  81. package/src/bridge/getTransactionStatus.ts +53 -22
  82. package/src/bridge/index.ts +2 -0
  83. package/src/bridge/prepareTransaction.ts +29 -4
  84. package/src/bridge/signOperation.ts +4 -0
  85. package/src/bridge/sync.test.ts +237 -191
  86. package/src/bridge/sync.ts +154 -24
  87. package/src/bridge/validateAddress.test.ts +25 -0
  88. package/src/bridge/validateAddress.ts +9 -0
  89. package/src/common-logic/account/getBalance.ts +2 -0
  90. package/src/common-logic/transaction/craftTransaction.ts +5 -0
  91. package/src/network/gateway.test.ts +169 -0
  92. package/src/network/gateway.ts +83 -12
  93. package/src/types/bridge.ts +2 -0
  94. package/tsconfig.json +7 -2
@@ -0,0 +1,132 @@
1
+ import BigNumber from "bignumber.js";
2
+ import { Operation } from "@ledgerhq/types-live";
3
+ import type { TokenAccount } from "@ledgerhq/types-live";
4
+ import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
5
+ import { encodeTokenAccountId, emptyHistoryCache } from "@ledgerhq/coin-framework/account/index";
6
+ import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
7
+ import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
8
+ import { type TransferProposal } from "../network/gateway";
9
+
10
+ export type CantonTokenAccount = TokenAccount & {
11
+ cantonResources: { pendingTransferProposals: TransferProposal[] };
12
+ };
13
+
14
+ export function buildSubAccounts({
15
+ accountId,
16
+ tokenBalances,
17
+ existingSubAccounts,
18
+ allOperations,
19
+ pendingTransferProposals,
20
+ calTokens,
21
+ }: {
22
+ accountId: string;
23
+ calTokens: Map<string, string>;
24
+ tokenBalances: Array<{
25
+ adminId: string;
26
+ totalBalance: bigint;
27
+ spendableBalance: bigint;
28
+ token: TokenCurrency;
29
+ }>;
30
+ existingSubAccounts: TokenAccount[];
31
+ allOperations: Operation[];
32
+ pendingTransferProposals: TransferProposal[];
33
+ }): CantonTokenAccount[] {
34
+ if (tokenBalances.length === 0) return [];
35
+
36
+ const tokenAccounts: CantonTokenAccount[] = [];
37
+ const existingAccountByTicker: { [ticker: string]: TokenAccount } = {};
38
+ const existingAccountTickers: string[] = [];
39
+
40
+ for (const existingSubAccount of existingSubAccounts) {
41
+ if (existingSubAccount.type === "TokenAccount") {
42
+ const { ticker } = existingSubAccount.token;
43
+ existingAccountTickers.push(ticker);
44
+ existingAccountByTicker[ticker] = existingSubAccount;
45
+ }
46
+ }
47
+
48
+ for (const { totalBalance, spendableBalance, token, adminId } of tokenBalances) {
49
+ const initialTokenAccount = existingAccountByTicker[token.ticker];
50
+
51
+ const instrumentId = calTokens.get(token.id) || token.name;
52
+
53
+ // Filter operations for this specific instrument (both id and admin must match)
54
+ const tokenOperations = allOperations.filter(op => {
55
+ const extra = op.extra as { instrumentId?: string; instrumentAdmin?: string };
56
+ return extra?.instrumentId === instrumentId && extra?.instrumentAdmin === adminId;
57
+ });
58
+
59
+ // Filter pending transfer proposals for this specific instrument
60
+ const tokenPendingTransferProposals = pendingTransferProposals.filter(
61
+ proposal => proposal.instrument_id === instrumentId && proposal.instrument_admin === adminId,
62
+ );
63
+
64
+ const tokenAccount = buildSubAccount({
65
+ totalBalance: new BigNumber(totalBalance.toString()),
66
+ spendableBalance: new BigNumber(spendableBalance.toString()),
67
+ accountId,
68
+ initialTokenAccount,
69
+ parentAccountId: accountId,
70
+ token,
71
+ operations: tokenOperations,
72
+ pendingTransferProposals: tokenPendingTransferProposals,
73
+ });
74
+ if (tokenAccount) tokenAccounts.push(tokenAccount);
75
+ }
76
+
77
+ return tokenAccounts;
78
+ }
79
+
80
+ function buildSubAccount({
81
+ totalBalance,
82
+ spendableBalance,
83
+ accountId,
84
+ initialTokenAccount,
85
+ parentAccountId,
86
+ token,
87
+ operations,
88
+ pendingTransferProposals,
89
+ }: {
90
+ totalBalance: BigNumber;
91
+ spendableBalance: BigNumber;
92
+ accountId: string;
93
+ initialTokenAccount: TokenAccount | undefined;
94
+ parentAccountId: string;
95
+ token: TokenCurrency;
96
+ operations: Operation[];
97
+ pendingTransferProposals: TransferProposal[];
98
+ }): CantonTokenAccount {
99
+ const subAccountId = encodeTokenAccountId(accountId, token);
100
+
101
+ // Merge old operations with new ones and update accountId for sub-account
102
+ const oldOperations = initialTokenAccount?.operations || [];
103
+ const newOperations = operations.map(op => ({
104
+ ...op,
105
+ id: encodeOperationId(subAccountId, op.hash, op.type),
106
+ accountId: subAccountId,
107
+ }));
108
+ const mergedOperations = mergeOps(oldOperations, newOperations);
109
+
110
+ const creationDate =
111
+ mergedOperations.length > 0
112
+ ? new Date(Math.min(...mergedOperations.map(op => op.date.getTime())))
113
+ : new Date();
114
+
115
+ return {
116
+ type: "TokenAccount" as const,
117
+ id: subAccountId,
118
+ parentId: parentAccountId,
119
+ token,
120
+ balance: totalBalance,
121
+ spendableBalance,
122
+ operationsCount: mergedOperations.length,
123
+ operations: mergedOperations,
124
+ creationDate,
125
+ pendingOperations: initialTokenAccount?.pendingOperations || [],
126
+ balanceHistoryCache: initialTokenAccount?.balanceHistoryCache || emptyHistoryCache,
127
+ swapHistory: [],
128
+ cantonResources: {
129
+ pendingTransferProposals,
130
+ },
131
+ };
132
+ }
@@ -3,7 +3,9 @@ import {
3
3
  FeeNotLoaded,
4
4
  FeeTooHigh,
5
5
  InvalidAddress,
6
+ NotEnoughBalance,
6
7
  NotEnoughBalanceBecauseDestinationNotCreated,
8
+ NotEnoughBalanceInParentAccount,
7
9
  NotEnoughSpendableBalance,
8
10
  RecipientRequired,
9
11
  } from "@ledgerhq/errors";
@@ -37,10 +39,22 @@ export const getTransactionStatus: AccountBridge<
37
39
  // reserveAmount is the minimum amount of currency that an account must hold in order to stay activated
38
40
  const reserveAmount = new BigNumber(coinConfig.getCoinConfig(account.currency).minReserve || 0);
39
41
  const estimatedFees = new BigNumber(transaction.fee || 0);
40
- const totalSpent = new BigNumber(transaction.amount).plus(estimatedFees);
41
42
  const amount = new BigNumber(transaction.amount);
42
43
 
43
- if (amount.gt(0) && estimatedFees.times(10).gt(amount)) {
44
+ const isTokenTransaction = !!transaction.subAccountId;
45
+ // For token transactions, fees are paid from parent account, so totalSpent is just amount
46
+ const totalSpent = isTokenTransaction ? amount : amount.plus(estimatedFees);
47
+
48
+ // Get the account balance to check against (subAccount for tokens, main account for native)
49
+ let accountBalance = account.spendableBalance;
50
+ if (isTokenTransaction && account.subAccounts) {
51
+ const subAccount = account.subAccounts.find(
52
+ sub => sub.type === "TokenAccount" && sub.id === transaction.subAccountId,
53
+ );
54
+ accountBalance = subAccount?.spendableBalance ?? new BigNumber(0);
55
+ }
56
+
57
+ if (amount.gt(0) && estimatedFees.times(10).gt(amount) && !isTokenTransaction) {
44
58
  // if the fee is more than 10 times the amount, we warn the user that fee is high compared to what he is sending
45
59
  warnings.feeTooHigh = new FeeTooHigh();
46
60
  }
@@ -48,24 +62,6 @@ export const getTransactionStatus: AccountBridge<
48
62
  if (transaction.fee === null || transaction.fee === undefined) {
49
63
  // if the fee is not loaded, we can't do much
50
64
  errors.fee = new FeeNotLoaded();
51
- } else if (totalSpent.gt(account.balance.minus(reserveAmount))) {
52
- // if the total spent is greater than the balance minus the reserve amount, tx is invalid
53
- errors.amount = new NotEnoughSpendableBalance("", {
54
- minimumAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
55
- disableRounding: true,
56
- useGrouping: false,
57
- showCode: true,
58
- }),
59
- });
60
- } else if (transaction.recipient && transaction.amount.lt(reserveAmount)) {
61
- // if we send an amount lower than reserve amount AND target account is new, we need to warn the user that the target account will not be activated
62
- errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", {
63
- minimalAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
64
- disableRounding: true,
65
- useGrouping: false,
66
- showCode: true,
67
- }),
68
- });
69
65
  }
70
66
 
71
67
  if (!transaction.recipient) {
@@ -77,11 +73,43 @@ export const getTransactionStatus: AccountBridge<
77
73
  });
78
74
  }
79
75
 
80
- if (!errors.amount && amount.eq(0)) {
76
+ if (amount.eq(0)) {
81
77
  // if the amount is 0, we prevent the user from sending the tx (even if it's technically feasible)
82
78
  errors.amount = new AmountRequired();
83
79
  }
84
80
 
81
+ // For token transactions, check that parent account has enough native balance to pay fees
82
+ if (isTokenTransaction && estimatedFees.gt(account.balance)) {
83
+ errors.amount = new NotEnoughBalanceInParentAccount();
84
+ }
85
+
86
+ // Check if total spent exceeds available balance
87
+ if (!errors.amount && totalSpent.gt(accountBalance)) {
88
+ errors.amount = new NotEnoughBalance();
89
+ }
90
+
91
+ // For native coin transactions, check reserve amount constraints
92
+ if (!isTokenTransaction && !errors.amount) {
93
+ if (totalSpent.gt(account.balance.minus(reserveAmount))) {
94
+ errors.amount = new NotEnoughSpendableBalance("", {
95
+ minimumAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
96
+ disableRounding: true,
97
+ useGrouping: false,
98
+ showCode: true,
99
+ }),
100
+ });
101
+ } else if (transaction.recipient && amount.lt(reserveAmount)) {
102
+ // if we send an amount lower than reserve amount AND target account is new, we need to warn the user that the target account will not be activated
103
+ errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", {
104
+ minimalAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
105
+ disableRounding: true,
106
+ useGrouping: false,
107
+ showCode: true,
108
+ }),
109
+ });
110
+ }
111
+ }
112
+
85
113
  const utxoWarning = validateUtxoCount(account, transaction);
86
114
  if (utxoWarning) {
87
115
  warnings.tooManyUtxos = utxoWarning;
@@ -120,7 +148,10 @@ function validateUtxoCount(account: CantonAccount, transaction: Transaction): Er
120
148
  }
121
149
 
122
150
  const { instrumentUtxoCounts } = account.cantonResources;
123
- const instrumentUtxoCount = instrumentUtxoCounts[transaction.tokenId] || 0;
151
+ const instrumentKey = transaction.instrumentAdmin
152
+ ? `${transaction.tokenId}-${transaction.instrumentAdmin}`
153
+ : transaction.tokenId;
154
+ const instrumentUtxoCount = instrumentUtxoCounts[instrumentKey] || 0;
124
155
 
125
156
  if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) {
126
157
  return new TooManyUtxosCritical();
@@ -23,6 +23,7 @@ import { updateTransaction } from "./updateTransaction";
23
23
  import { buildOnboardAccount, buildAuthorizePreapproval } from "./onboard";
24
24
  import { buildTransferInstruction } from "./acceptOffer";
25
25
  import { assignToAccountRaw, assignFromAccountRaw } from "./serialization";
26
+ import { validateAddress } from "./validateAddress";
26
27
 
27
28
  export function createBridges(
28
29
  signerContext: SignerContext<CantonSigner>,
@@ -70,6 +71,7 @@ export function createBridges(
70
71
  assignToAccountRaw,
71
72
  assignFromAccountRaw,
72
73
  getSerializedAddressParameters,
74
+ validateAddress,
73
75
  };
74
76
 
75
77
  return {
@@ -1,9 +1,14 @@
1
- import { AccountBridge } from "@ledgerhq/types-live";
1
+ import { AccountBridge, TokenAccount } from "@ledgerhq/types-live";
2
2
  import { Transaction } from "../types";
3
3
  import { estimateFees } from "../common-logic";
4
4
  import BigNumber from "bignumber.js";
5
5
  import { updateTransaction } from "./updateTransaction";
6
6
  import coinConfig from "../config";
7
+ import { getCalTokensCached } from "../network/gateway";
8
+
9
+ type CantonTokenAccount = TokenAccount & {
10
+ cantonResources: { instrumentAdmin: string };
11
+ };
7
12
 
8
13
  export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"] = async (
9
14
  account,
@@ -14,8 +19,28 @@ export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"
14
19
  (await estimateFees(account.currency, BigInt(amount.toFixed()))).toString(),
15
20
  );
16
21
 
17
- if (!transaction.tokenId) {
18
- transaction.tokenId = coinConfig.getCoinConfig(account.currency).nativeInstrumentId;
22
+ let tokenId = transaction.tokenId;
23
+ let instrumentAdmin: string | undefined;
24
+ if (transaction.subAccountId && account.subAccounts) {
25
+ const subAccount = account.subAccounts.find(
26
+ (sub): sub is CantonTokenAccount =>
27
+ sub.type === "TokenAccount" && sub.id === transaction.subAccountId,
28
+ );
29
+ if (subAccount) {
30
+ const calTokens = await getCalTokensCached(account.currency);
31
+ tokenId = calTokens.get(subAccount.token.id) || subAccount.token.name;
32
+ instrumentAdmin = subAccount.token.contractAddress;
33
+ }
19
34
  }
20
- return updateTransaction(transaction, { fee });
35
+
36
+ // Default to native instrument if no tokenId set
37
+ if (!tokenId) {
38
+ tokenId = coinConfig.getCoinConfig(account.currency).nativeInstrumentId;
39
+ }
40
+
41
+ return updateTransaction(transaction, {
42
+ fee,
43
+ tokenId,
44
+ ...(instrumentAdmin !== undefined && { instrumentAdmin }),
45
+ });
21
46
  };
@@ -32,12 +32,16 @@ export const buildSignOperation =
32
32
  tokenId: string;
33
33
  expireInSeconds: number;
34
34
  memo?: string;
35
+ instrumentAdmin?: string;
35
36
  } = {
36
37
  recipient: transaction.recipient,
37
38
  amount: transaction.amount,
38
39
  expireInSeconds: 60 * 60,
39
40
  tokenId: transaction.tokenId,
40
41
  };
42
+ if (transaction.instrumentAdmin) {
43
+ params.instrumentAdmin = transaction.instrumentAdmin;
44
+ }
41
45
  if (transaction.memo) {
42
46
  params.memo = transaction.memo;
43
47
  }