@ledgerhq/coin-canton 0.9.0-nightly.1 → 0.9.0-nightly.20251030160608

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 (205) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.unimportedrc.json +2 -1
  3. package/CHANGELOG.md +26 -9
  4. package/README.md +75 -0
  5. package/lib/bridge/deviceTransactionConfig.d.ts +1 -1
  6. package/lib/bridge/deviceTransactionConfig.d.ts.map +1 -1
  7. package/lib/bridge/deviceTransactionConfig.js +1 -1
  8. package/lib/bridge/deviceTransactionConfig.js.map +1 -1
  9. package/lib/bridge/estimateMaxSpendable.d.ts +2 -2
  10. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  11. package/lib/bridge/estimateMaxSpendable.js +2 -3
  12. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  13. package/lib/bridge/getTransactionStatus.d.ts +5 -3
  14. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  15. package/lib/bridge/getTransactionStatus.js +27 -10
  16. package/lib/bridge/getTransactionStatus.js.map +1 -1
  17. package/lib/bridge/index.d.ts +2 -2
  18. package/lib/bridge/index.d.ts.map +1 -1
  19. package/lib/bridge/index.js +3 -3
  20. package/lib/bridge/index.js.map +1 -1
  21. package/lib/bridge/onboard.d.ts.map +1 -1
  22. package/lib/bridge/onboard.js +48 -22
  23. package/lib/bridge/onboard.js.map +1 -1
  24. package/lib/bridge/prepareTransaction.js +1 -1
  25. package/lib/bridge/prepareTransaction.js.map +1 -1
  26. package/lib/bridge/serialization.d.ts +4 -0
  27. package/lib/bridge/serialization.d.ts.map +1 -0
  28. package/lib/bridge/serialization.js +36 -0
  29. package/lib/bridge/serialization.js.map +1 -0
  30. package/lib/bridge/signOperation.d.ts.map +1 -1
  31. package/lib/bridge/signOperation.js +11 -4
  32. package/lib/bridge/signOperation.js.map +1 -1
  33. package/lib/bridge/sync.d.ts.map +1 -1
  34. package/lib/bridge/sync.js +14 -6
  35. package/lib/bridge/sync.js.map +1 -1
  36. package/lib/bridge/transaction.js +1 -1
  37. package/lib/bridge/transaction.js.map +1 -1
  38. package/lib/common-logic/account/getBalance.d.ts +5 -1
  39. package/lib/common-logic/account/getBalance.d.ts.map +1 -1
  40. package/lib/common-logic/account/getBalance.js +2 -0
  41. package/lib/common-logic/account/getBalance.js.map +1 -1
  42. package/lib/common-logic/index.d.ts +1 -0
  43. package/lib/common-logic/index.d.ts.map +1 -1
  44. package/lib/common-logic/index.js +3 -1
  45. package/lib/common-logic/index.js.map +1 -1
  46. package/lib/common-logic/transaction/craftTransaction.js +1 -1
  47. package/lib/common-logic/transaction/craftTransaction.js.map +1 -1
  48. package/lib/common-logic/transaction/estimateFees.js +1 -1
  49. package/lib/common-logic/transaction/estimateFees.js.map +1 -1
  50. package/lib/common-logic/transaction/sign.d.ts +8 -0
  51. package/lib/common-logic/transaction/sign.d.ts.map +1 -0
  52. package/lib/common-logic/transaction/sign.js +45 -0
  53. package/lib/common-logic/transaction/sign.js.map +1 -0
  54. package/lib/common-logic/transaction/split.d.ts +9 -0
  55. package/lib/common-logic/transaction/split.d.ts.map +1 -0
  56. package/lib/common-logic/transaction/split.js +119 -0
  57. package/lib/common-logic/transaction/split.js.map +1 -0
  58. package/lib/common-logic/utils.js +2 -2
  59. package/lib/common-logic/utils.js.map +1 -1
  60. package/lib/network/gateway.d.ts +6 -1
  61. package/lib/network/gateway.d.ts.map +1 -1
  62. package/lib/network/gateway.js +6 -9
  63. package/lib/network/gateway.js.map +1 -1
  64. package/lib/test/cantonTestUtils.d.ts +4 -9
  65. package/lib/test/cantonTestUtils.d.ts.map +1 -1
  66. package/lib/test/cantonTestUtils.js +736 -27
  67. package/lib/test/cantonTestUtils.js.map +1 -1
  68. package/lib/test/fixtures.d.ts +5 -0
  69. package/lib/test/fixtures.d.ts.map +1 -0
  70. package/lib/test/fixtures.js +57 -0
  71. package/lib/test/fixtures.js.map +1 -0
  72. package/lib/types/bridge.d.ts +14 -2
  73. package/lib/types/bridge.d.ts.map +1 -1
  74. package/lib/types/errors.d.ts +6 -0
  75. package/lib/types/errors.d.ts.map +1 -1
  76. package/lib/types/errors.js +3 -1
  77. package/lib/types/errors.js.map +1 -1
  78. package/lib/types/index.d.ts +1 -0
  79. package/lib/types/index.d.ts.map +1 -1
  80. package/lib/types/index.js +1 -0
  81. package/lib/types/index.js.map +1 -1
  82. package/lib/types/signer.d.ts +15 -2
  83. package/lib/types/signer.d.ts.map +1 -1
  84. package/lib/types/transaction-proto.json +1238 -0
  85. package/lib-es/bridge/deviceTransactionConfig.d.ts +1 -1
  86. package/lib-es/bridge/deviceTransactionConfig.d.ts.map +1 -1
  87. package/lib-es/bridge/deviceTransactionConfig.js +1 -1
  88. package/lib-es/bridge/deviceTransactionConfig.js.map +1 -1
  89. package/lib-es/bridge/estimateMaxSpendable.d.ts +2 -2
  90. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  91. package/lib-es/bridge/estimateMaxSpendable.js +2 -3
  92. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  93. package/lib-es/bridge/getTransactionStatus.d.ts +5 -3
  94. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  95. package/lib-es/bridge/getTransactionStatus.js +27 -10
  96. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  97. package/lib-es/bridge/index.d.ts +2 -2
  98. package/lib-es/bridge/index.d.ts.map +1 -1
  99. package/lib-es/bridge/index.js +3 -3
  100. package/lib-es/bridge/index.js.map +1 -1
  101. package/lib-es/bridge/onboard.d.ts.map +1 -1
  102. package/lib-es/bridge/onboard.js +49 -23
  103. package/lib-es/bridge/onboard.js.map +1 -1
  104. package/lib-es/bridge/prepareTransaction.js +1 -1
  105. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  106. package/lib-es/bridge/serialization.d.ts +4 -0
  107. package/lib-es/bridge/serialization.d.ts.map +1 -0
  108. package/lib-es/bridge/serialization.js +32 -0
  109. package/lib-es/bridge/serialization.js.map +1 -0
  110. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  111. package/lib-es/bridge/signOperation.js +11 -4
  112. package/lib-es/bridge/signOperation.js.map +1 -1
  113. package/lib-es/bridge/sync.d.ts.map +1 -1
  114. package/lib-es/bridge/sync.js +14 -6
  115. package/lib-es/bridge/sync.js.map +1 -1
  116. package/lib-es/bridge/transaction.js +1 -1
  117. package/lib-es/bridge/transaction.js.map +1 -1
  118. package/lib-es/common-logic/account/getBalance.d.ts +5 -1
  119. package/lib-es/common-logic/account/getBalance.d.ts.map +1 -1
  120. package/lib-es/common-logic/account/getBalance.js +2 -0
  121. package/lib-es/common-logic/account/getBalance.js.map +1 -1
  122. package/lib-es/common-logic/index.d.ts +1 -0
  123. package/lib-es/common-logic/index.d.ts.map +1 -1
  124. package/lib-es/common-logic/index.js +1 -0
  125. package/lib-es/common-logic/index.js.map +1 -1
  126. package/lib-es/common-logic/transaction/craftTransaction.js +1 -1
  127. package/lib-es/common-logic/transaction/craftTransaction.js.map +1 -1
  128. package/lib-es/common-logic/transaction/estimateFees.js +1 -1
  129. package/lib-es/common-logic/transaction/estimateFees.js.map +1 -1
  130. package/lib-es/common-logic/transaction/sign.d.ts +8 -0
  131. package/lib-es/common-logic/transaction/sign.d.ts.map +1 -0
  132. package/lib-es/common-logic/transaction/sign.js +42 -0
  133. package/lib-es/common-logic/transaction/sign.js.map +1 -0
  134. package/lib-es/common-logic/transaction/split.d.ts +9 -0
  135. package/lib-es/common-logic/transaction/split.d.ts.map +1 -0
  136. package/lib-es/common-logic/transaction/split.js +83 -0
  137. package/lib-es/common-logic/transaction/split.js.map +1 -0
  138. package/lib-es/common-logic/utils.js +2 -2
  139. package/lib-es/common-logic/utils.js.map +1 -1
  140. package/lib-es/network/gateway.d.ts +6 -1
  141. package/lib-es/network/gateway.d.ts.map +1 -1
  142. package/lib-es/network/gateway.js +5 -10
  143. package/lib-es/network/gateway.js.map +1 -1
  144. package/lib-es/test/cantonTestUtils.d.ts +4 -9
  145. package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
  146. package/lib-es/test/cantonTestUtils.js +697 -21
  147. package/lib-es/test/cantonTestUtils.js.map +1 -1
  148. package/lib-es/test/fixtures.d.ts +5 -0
  149. package/lib-es/test/fixtures.d.ts.map +1 -0
  150. package/lib-es/test/fixtures.js +49 -0
  151. package/lib-es/test/fixtures.js.map +1 -0
  152. package/lib-es/types/bridge.d.ts +14 -2
  153. package/lib-es/types/bridge.d.ts.map +1 -1
  154. package/lib-es/types/errors.d.ts +6 -0
  155. package/lib-es/types/errors.d.ts.map +1 -1
  156. package/lib-es/types/errors.js +2 -0
  157. package/lib-es/types/errors.js.map +1 -1
  158. package/lib-es/types/index.d.ts +1 -0
  159. package/lib-es/types/index.d.ts.map +1 -1
  160. package/lib-es/types/index.js +1 -0
  161. package/lib-es/types/index.js.map +1 -1
  162. package/lib-es/types/signer.d.ts +15 -2
  163. package/lib-es/types/signer.d.ts.map +1 -1
  164. package/lib-es/types/transaction-proto.json +1238 -0
  165. package/package.json +13 -10
  166. package/scripts/generate.js +261 -0
  167. package/src/bridge/deviceTransactionConfig.test.ts +14 -10
  168. package/src/bridge/deviceTransactionConfig.ts +2 -2
  169. package/src/bridge/estimateMaxSpendable.ts +6 -8
  170. package/src/bridge/getTransactionStatus.test.ts +103 -165
  171. package/src/bridge/getTransactionStatus.ts +43 -11
  172. package/src/bridge/index.ts +6 -5
  173. package/src/bridge/onboard.integ.test.ts +8 -51
  174. package/src/bridge/onboard.ts +58 -33
  175. package/src/bridge/prepareTransaction.ts +1 -1
  176. package/src/bridge/serialization.ts +44 -0
  177. package/src/bridge/signOperation.test.ts +123 -0
  178. package/src/bridge/signOperation.ts +13 -6
  179. package/src/bridge/sync.integ.test.ts +157 -132
  180. package/src/bridge/sync.test.ts +5 -1
  181. package/src/bridge/sync.ts +18 -7
  182. package/src/bridge/transaction.ts +1 -1
  183. package/src/common-logic/account/getBalance.ts +12 -2
  184. package/src/common-logic/account/getBalance.unit.test.ts +7 -1
  185. package/src/common-logic/index.ts +1 -0
  186. package/src/common-logic/transaction/craftTransaction.ts +1 -1
  187. package/src/common-logic/transaction/estimateFees.test.ts +10 -0
  188. package/src/common-logic/transaction/estimateFees.ts +1 -1
  189. package/src/common-logic/transaction/sign.test.ts +389 -0
  190. package/src/common-logic/transaction/sign.ts +59 -0
  191. package/src/common-logic/transaction/split.test.ts +50 -0
  192. package/src/common-logic/transaction/split.ts +101 -0
  193. package/src/common-logic/utils.test.ts +22 -30
  194. package/src/common-logic/utils.ts +2 -2
  195. package/src/network/gateway.integ.test.ts +5 -6
  196. package/src/network/gateway.ts +13 -10
  197. package/src/test/cantonTestUtils.ts +789 -24
  198. package/src/test/fixtures.ts +53 -0
  199. package/src/test/prepare-transfer-serialized.json +26 -0
  200. package/src/test/prepare-transfer.json +3298 -0
  201. package/src/types/bridge.ts +15 -2
  202. package/src/types/errors.ts +3 -0
  203. package/src/types/index.ts +1 -0
  204. package/src/types/signer.ts +21 -3
  205. package/src/types/transaction-proto.json +1238 -0
@@ -52,6 +52,7 @@ describe("makeGetAccountShape", () => {
52
52
  mockedCoinConfig.mockReturnValue({
53
53
  nativeInstrumentId: "Native",
54
54
  minReserve: "0",
55
+ useGateway: true,
55
56
  });
56
57
 
57
58
  mockedIsAuthorized.mockResolvedValue(true);
@@ -64,6 +65,7 @@ describe("makeGetAccountShape", () => {
64
65
  instrument_id: "Native",
65
66
  amount: "1000",
66
67
  locked: false,
68
+ utxo_count: 1,
67
69
  },
68
70
  ]);
69
71
  mockedGetOperations.mockResolvedValue({
@@ -113,11 +115,13 @@ describe("makeGetAccountShape", () => {
113
115
  instrument_id: "LockedNative",
114
116
  amount: "1000",
115
117
  locked: true,
118
+ utxo_count: 1,
116
119
  },
117
120
  {
118
121
  instrument_id: "Native",
119
122
  amount: "10",
120
123
  locked: false,
124
+ utxo_count: 1,
121
125
  },
122
126
  ]);
123
127
 
@@ -128,7 +132,7 @@ describe("makeGetAccountShape", () => {
128
132
 
129
133
  expect(shape).toBeDefined();
130
134
  expect(shape.balance).toEqual(BigNumber(1010));
131
- expect(shape.spendableBalance).toEqual(BigNumber(10));
135
+ expect(shape.spendableBalance).toEqual(BigNumber(1010));
132
136
  });
133
137
 
134
138
  it("should handle empty balances correctly", async () => {
@@ -4,7 +4,8 @@ import { encodeAccountId } from "@ledgerhq/coin-framework/account/index";
4
4
  import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
5
5
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
6
6
  import { SignerContext } from "@ledgerhq/coin-framework/signer";
7
- import { getBalance, getLedgerEnd, getOperations, type OperationInfo } from "../network/gateway";
7
+ import { getLedgerEnd, getOperations, type OperationInfo } from "../network/gateway";
8
+ import { getBalance, type CantonBalance } from "../common-logic/account/getBalance";
8
9
  import coinConfig from "../config";
9
10
  import resolver from "../signer";
10
11
  import { CantonAccount, CantonSigner } from "../types";
@@ -108,18 +109,25 @@ export function makeGetAccountShape(
108
109
  const balances = xpubOrAddress ? await getBalance(currency, xpubOrAddress) : [];
109
110
 
110
111
  const balancesData = (balances || []).reduce(
111
- (acc, { amount, instrument_id, locked }) => {
112
- acc[instrument_id] = { amount, locked };
112
+ (acc, balance) => {
113
+ acc[balance.instrumentId] = balance;
113
114
  return acc;
114
115
  },
115
- {} as Record<string, { amount: string; locked: boolean }>,
116
+ {} as Record<string, CantonBalance>,
116
117
  );
117
118
 
118
- const unlockedAmount = new BigNumber(balancesData[nativeInstrumentId]?.amount || "0");
119
- const lockedAmount = new BigNumber(balancesData[`Locked${nativeInstrumentId}`]?.amount || "0");
119
+ const unlockedAmount = new BigNumber(balancesData[nativeInstrumentId]?.value.toString() || "0");
120
+ const lockedAmount = new BigNumber(
121
+ balancesData[`Locked${nativeInstrumentId}`]?.value.toString() || "0",
122
+ );
120
123
  const totalBalance = unlockedAmount.plus(lockedAmount);
121
124
  const reserveMin = new BigNumber(coinConfig.getCoinConfig(currency).minReserve || 0);
122
- const spendableBalance = BigNumber.max(0, unlockedAmount.minus(reserveMin));
125
+ const spendableBalance = BigNumber.max(0, totalBalance.minus(reserveMin));
126
+
127
+ const instrumentUtxoCounts: Record<string, number> = {};
128
+ for (const [instrumentId, balance] of Object.entries(balancesData)) {
129
+ instrumentUtxoCounts[instrumentId] = balance.utxoCount;
130
+ }
123
131
 
124
132
  let operations: Operation[] = [];
125
133
  if (xpubOrAddress) {
@@ -159,6 +167,9 @@ export function makeGetAccountShape(
159
167
  spendableBalance,
160
168
  xpub: xpubOrAddress,
161
169
  used,
170
+ cantonResources: {
171
+ instrumentUtxoCounts,
172
+ },
162
173
  };
163
174
 
164
175
  return shape;
@@ -48,7 +48,7 @@ export const toTransactionRaw = (t: Transaction): TransactionRaw => {
48
48
  return {
49
49
  ...common,
50
50
  family: t.family,
51
- fee: t.fee ? t.fee.toString() : null,
51
+ fee: t.fee ? t.fee.toFixed() : null,
52
52
  tokenId: t.tokenId,
53
53
  };
54
54
  };
@@ -3,12 +3,17 @@ import { getBalance as gatewayGetBalance, type InstrumentBalance } from "../../n
3
3
  import coinConfig from "../../config";
4
4
  import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
5
5
 
6
+ export type CantonBalance = Balance & {
7
+ utxoCount: number;
8
+ instrumentId: string;
9
+ };
10
+
6
11
  const useGateway = (currency: CryptoCurrency) =>
7
12
  coinConfig.getCoinConfig(currency).useGateway === true;
8
13
  const getNativeId = (currency: CryptoCurrency) =>
9
14
  coinConfig.getCoinConfig(currency).nativeInstrumentId;
10
15
 
11
- function adaptInstrument(currency: CryptoCurrency, instrument: InstrumentBalance): Balance {
16
+ function adaptInstrument(currency: CryptoCurrency, instrument: InstrumentBalance): CantonBalance {
12
17
  return {
13
18
  value: BigInt(instrument.amount),
14
19
  locked: instrument.locked === true ? BigInt(instrument.amount) : BigInt(0),
@@ -16,10 +21,15 @@ function adaptInstrument(currency: CryptoCurrency, instrument: InstrumentBalance
16
21
  getNativeId(currency) === instrument.instrument_id
17
22
  ? { type: "native" }
18
23
  : { type: "token", assetReference: instrument.instrument_id },
24
+ utxoCount: instrument.utxo_count,
25
+ instrumentId: instrument.instrument_id,
19
26
  };
20
27
  }
21
28
 
22
- export async function getBalance(currency: CryptoCurrency, partyId: string): Promise<Balance[]> {
29
+ export async function getBalance(
30
+ currency: CryptoCurrency,
31
+ partyId: string,
32
+ ): Promise<CantonBalance[]> {
23
33
  if (useGateway(currency))
24
34
  return (await gatewayGetBalance(currency, partyId)).map(instrument =>
25
35
  adaptInstrument(currency, instrument),
@@ -29,11 +29,13 @@ describe("getBalance", () => {
29
29
  {
30
30
  instrument_id: "native-id",
31
31
  amount: "1000",
32
+ utxo_count: 1,
32
33
  locked: false,
33
34
  },
34
35
  {
35
36
  instrument_id: "token-123",
36
37
  amount: "5000",
38
+ utxo_count: 1,
37
39
  locked: true,
38
40
  },
39
41
  ];
@@ -43,16 +45,20 @@ describe("getBalance", () => {
43
45
  const result = await getBalance(mockCurrency, "party-id");
44
46
 
45
47
  expect(getBalanceFromNetwork).toHaveBeenCalledWith(mockCurrency, "party-id");
46
- expect(result).toEqual<Balance[]>([
48
+ expect(result).toEqual([
47
49
  {
48
50
  value: BigInt(1000),
49
51
  locked: BigInt(0),
50
52
  asset: { type: "native" },
53
+ utxoCount: 1,
54
+ instrumentId: "native-id",
51
55
  },
52
56
  {
53
57
  value: BigInt(5000),
54
58
  locked: BigInt(5000),
55
59
  asset: { type: "token", assetReference: "token-123" },
60
+ utxoCount: 1,
61
+ instrumentId: "token-123",
56
62
  },
57
63
  ]);
58
64
  });
@@ -2,6 +2,7 @@ export { broadcast } from "./transaction/broadcast";
2
2
  export { combine } from "./transaction/combine";
3
3
  export { craftTransaction } from "./transaction/craftTransaction";
4
4
  export { estimateFees } from "./transaction/estimateFees";
5
+ export { signTransaction } from "./transaction/sign";
5
6
  export { getBalance } from "./account/getBalance";
6
7
  export { lastBlock } from "./history/lastBlock";
7
8
  export { listOperations } from "./history/listOperations";
@@ -27,7 +27,7 @@ export async function craftTransaction(
27
27
  }> {
28
28
  const params: PrepareTransferRequest = {
29
29
  recipient: transaction.recipient || "",
30
- amount: transaction.amount.toString(),
30
+ amount: transaction.amount.toFixed(),
31
31
  type: "token-transfer-request" as const,
32
32
  execute_before_secs: transaction.expireInSeconds,
33
33
  instrument_id: transaction.tokenId,
@@ -47,4 +47,14 @@ describe("estimateFees", () => {
47
47
  // 3 CC
48
48
  expect(result).toEqual(3n * magnitude);
49
49
  });
50
+
51
+ it("returns forced fees when 0 is setup in config", async () => {
52
+ mockGetCoinConfig.mockReturnValue({
53
+ fee: 0,
54
+ } as unknown as coinConfigModule.CantonCoinConfig);
55
+
56
+ const result = await estimateFees(mockCurrency, 1000n * magnitude);
57
+
58
+ expect(result).toEqual(0n);
59
+ });
50
60
  });
@@ -21,7 +21,7 @@ const feeValue = (currency: CryptoCurrency) => coinConfig.getCoinConfig(currency
21
21
 
22
22
  export async function estimateFees(currency: CryptoCurrency, amount: bigint): Promise<bigint> {
23
23
  const forcedValue = feeValue(currency);
24
- if (forcedValue) {
24
+ if (forcedValue !== undefined) {
25
25
  return Promise.resolve(BigInt(forcedValue) * magnitude);
26
26
  } else {
27
27
  return Promise.resolve(
@@ -0,0 +1,389 @@
1
+ import { OnboardingPrepareResponse, PrepareTransferResponse } from "../../network/gateway";
2
+ import {
3
+ CantonPreparedTransaction,
4
+ CantonSigner,
5
+ CantonSignature,
6
+ CantonUntypedVersionedMessage,
7
+ } from "../../types/signer";
8
+ import { signTransaction } from "./sign";
9
+
10
+ class MockCantonSigner implements CantonSigner {
11
+ async getAddress(path: string, display?: boolean) {
12
+ return {
13
+ publicKey: "mock-public-key",
14
+ address: "mock-address",
15
+ path,
16
+ };
17
+ }
18
+
19
+ async signTransaction(
20
+ path: string,
21
+ data: CantonPreparedTransaction | CantonUntypedVersionedMessage | string,
22
+ ): Promise<CantonSignature> {
23
+ if (typeof data === "string") {
24
+ return { signature: `txhash-signature-${data}` };
25
+ } else if ("transactions" in data) {
26
+ const result: CantonSignature = {
27
+ signature: `untyped-signature-${data.transactions.length}`,
28
+ };
29
+ if (data.challenge) {
30
+ result.applicationSignature = `challenge-signature-${data.challenge}`;
31
+ }
32
+ return result;
33
+ } else {
34
+ return {
35
+ signature: `prepared-transaction-signature-${data.damlTransaction.length}-${data.nodes.length}`,
36
+ };
37
+ }
38
+ }
39
+ }
40
+
41
+ describe("signTransaction", () => {
42
+ const mockSigner = new MockCantonSigner();
43
+ const mockDerivationPath = "44'/6767'/0'/0'/0'";
44
+
45
+ it("should sign prepared transaction", async () => {
46
+ // GIVEN
47
+ const mockPrepareTransferResponse: PrepareTransferResponse = {
48
+ json: {
49
+ transaction: {
50
+ version: "2.1",
51
+ roots: ["0"],
52
+ nodes: [
53
+ {
54
+ nodeId: "0",
55
+ v1: {
56
+ create: {
57
+ lfVersion: "2.1",
58
+ contractId: "test-contract-id",
59
+ packageName: "test-package",
60
+ templateId: {
61
+ packageId: "test-package-id",
62
+ moduleName: "TestModule",
63
+ entityName: "TestEntity",
64
+ },
65
+ argument: {
66
+ record: {
67
+ recordId: {
68
+ packageId: "test-package-id",
69
+ moduleName: "TestModule",
70
+ entityName: "TestEntity",
71
+ },
72
+ fields: [],
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ ],
79
+ },
80
+ metadata: {
81
+ submitterInfo: {
82
+ actAs: ["test::party"],
83
+ commandId: "test-command-id",
84
+ },
85
+ synchronizerId: "test-synchronizer-id",
86
+ transactionUuid: "test-transaction-uuid",
87
+ submissionTime: "1234567890",
88
+ inputContracts: [],
89
+ },
90
+ },
91
+ serialized: "serialized-transaction",
92
+ hash: "test-hash",
93
+ };
94
+
95
+ // WHEN
96
+ const result = await signTransaction(
97
+ mockSigner,
98
+ mockDerivationPath,
99
+ mockPrepareTransferResponse,
100
+ );
101
+
102
+ // THEN
103
+ expect(result).toEqual({ signature: "prepared-transaction-signature-10-1" });
104
+ });
105
+
106
+ it("should sign untyped versioned message", async () => {
107
+ // GIVEN
108
+ const mockOnboardingPrepareResponse: OnboardingPrepareResponse = {
109
+ party_id: "test-party-id",
110
+ party_name: "test-party-name",
111
+ public_key_fingerprint: "test-fingerprint",
112
+ transactions: {
113
+ namespace_transaction: {
114
+ serialized: "namespace-transaction-data",
115
+ json: {},
116
+ hash: "namespace-hash",
117
+ },
118
+ party_to_key_transaction: {
119
+ serialized: "party-to-key-transaction-data",
120
+ json: {},
121
+ hash: "party-to-key-hash",
122
+ },
123
+ party_to_participant_transaction: {
124
+ serialized: "party-to-participant-transaction-data",
125
+ json: {},
126
+ hash: "party-to-participant-hash",
127
+ },
128
+ combined_hash: "combined-hash",
129
+ },
130
+ };
131
+
132
+ // WHEN
133
+ const result = await signTransaction(
134
+ mockSigner,
135
+ mockDerivationPath,
136
+ mockOnboardingPrepareResponse,
137
+ );
138
+
139
+ // THEN
140
+ expect(result).toEqual({ signature: "untyped-signature-3" });
141
+ });
142
+
143
+ it("should handle empty signature from signer", async () => {
144
+ // GIVEN
145
+ const mockSignerWithEmptySignature = {
146
+ ...mockSigner,
147
+ signTransaction: jest.fn().mockResolvedValue(""),
148
+ } as unknown as CantonSigner;
149
+
150
+ const mockPrepareTransferResponse: PrepareTransferResponse = {
151
+ json: {
152
+ transaction: {
153
+ version: "2.1",
154
+ roots: ["0"],
155
+ nodes: [],
156
+ },
157
+ metadata: {
158
+ submitterInfo: {
159
+ actAs: ["test::party"],
160
+ commandId: "test-command-id",
161
+ },
162
+ synchronizerId: "test-synchronizer-id",
163
+ transactionUuid: "test-transaction-uuid",
164
+ submissionTime: "1234567890",
165
+ inputContracts: [],
166
+ },
167
+ },
168
+ serialized: "serialized-transaction",
169
+ hash: "test-hash",
170
+ };
171
+
172
+ // WHEN & THEN
173
+ await expect(
174
+ signTransaction(
175
+ mockSignerWithEmptySignature,
176
+ mockDerivationPath,
177
+ mockPrepareTransferResponse,
178
+ ),
179
+ ).rejects.toThrow("Device returned empty signature");
180
+ });
181
+
182
+ it("should handle signer errors", async () => {
183
+ // GIVEN
184
+ const mockSignerWithError = {
185
+ ...mockSigner,
186
+ signTransaction: jest.fn().mockRejectedValue(new Error("Signer error")),
187
+ } as unknown as CantonSigner;
188
+
189
+ const mockPrepareTransferResponse: PrepareTransferResponse = {
190
+ json: {
191
+ transaction: {
192
+ version: "2.1",
193
+ roots: ["0"],
194
+ nodes: [],
195
+ },
196
+ metadata: {
197
+ submitterInfo: {
198
+ actAs: ["test::party"],
199
+ commandId: "test-command-id",
200
+ },
201
+ synchronizerId: "test-synchronizer-id",
202
+ transactionUuid: "test-transaction-uuid",
203
+ submissionTime: "1234567890",
204
+ inputContracts: [],
205
+ },
206
+ },
207
+ serialized: "serialized-transaction",
208
+ hash: "test-hash",
209
+ };
210
+
211
+ // WHEN & THEN
212
+ await expect(
213
+ signTransaction(mockSignerWithError, mockDerivationPath, mockPrepareTransferResponse),
214
+ ).rejects.toThrow("Signer error");
215
+ });
216
+
217
+ it("should call signer with correct parameters for prepared transaction", async () => {
218
+ // GIVEN
219
+ const mockSignerSpy = jest.fn().mockResolvedValue({ signature: "test-signature" });
220
+ const mockSignerWithSpy = {
221
+ ...mockSigner,
222
+ signTransaction: mockSignerSpy,
223
+ } as unknown as CantonSigner;
224
+
225
+ const mockPrepareTransferResponse: PrepareTransferResponse = {
226
+ json: {
227
+ transaction: {
228
+ version: "2.1",
229
+ roots: ["0"],
230
+ nodes: [
231
+ {
232
+ nodeId: "0",
233
+ v1: {
234
+ create: {
235
+ lfVersion: "2.1",
236
+ contractId: "test-contract-id",
237
+ packageName: "test-package",
238
+ templateId: {
239
+ packageId: "test-package-id",
240
+ moduleName: "TestModule",
241
+ entityName: "TestEntity",
242
+ },
243
+ argument: {
244
+ record: {
245
+ recordId: {
246
+ packageId: "test-package-id",
247
+ moduleName: "TestModule",
248
+ entityName: "TestEntity",
249
+ },
250
+ fields: [],
251
+ },
252
+ },
253
+ },
254
+ },
255
+ },
256
+ ],
257
+ },
258
+ metadata: {
259
+ submitterInfo: {
260
+ actAs: ["test::party"],
261
+ commandId: "test-command-id",
262
+ },
263
+ synchronizerId: "test-synchronizer-id",
264
+ transactionUuid: "test-transaction-uuid",
265
+ submissionTime: "1234567890",
266
+ inputContracts: [],
267
+ },
268
+ },
269
+ serialized: "serialized-transaction",
270
+ hash: "test-hash",
271
+ };
272
+
273
+ // WHEN
274
+ await signTransaction(mockSignerWithSpy, mockDerivationPath, mockPrepareTransferResponse);
275
+
276
+ // THEN
277
+ expect(mockSignerSpy).toHaveBeenCalledWith(
278
+ mockDerivationPath,
279
+ expect.objectContaining({
280
+ damlTransaction: expect.any(Uint8Array),
281
+ nodes: expect.any(Array),
282
+ metadata: expect.any(Uint8Array),
283
+ inputContracts: expect.any(Array),
284
+ }),
285
+ );
286
+ });
287
+
288
+ it("should call signer with correct parameters for untyped versioned message without challenge", async () => {
289
+ // GIVEN
290
+ const mockSignerSpy = jest.fn().mockResolvedValue({ signature: "test-signature" });
291
+ const mockSignerWithSpy = {
292
+ ...mockSigner,
293
+ signTransaction: mockSignerSpy,
294
+ } as unknown as CantonSigner;
295
+
296
+ const mockOnboardingPrepareResponse: OnboardingPrepareResponse = {
297
+ party_id: "test-party-id",
298
+ party_name: "test-party-name",
299
+ public_key_fingerprint: "test-fingerprint",
300
+ transactions: {
301
+ namespace_transaction: {
302
+ serialized: "namespace-transaction-data",
303
+ json: {},
304
+ hash: "namespace-hash",
305
+ },
306
+ party_to_key_transaction: {
307
+ serialized: "party-to-key-transaction-data",
308
+ json: {},
309
+ hash: "party-to-key-hash",
310
+ },
311
+ party_to_participant_transaction: {
312
+ serialized: "party-to-participant-transaction-data",
313
+ json: {},
314
+ hash: "party-to-participant-hash",
315
+ },
316
+ combined_hash: "combined-hash",
317
+ },
318
+ };
319
+
320
+ // WHEN
321
+ await signTransaction(mockSignerWithSpy, mockDerivationPath, mockOnboardingPrepareResponse);
322
+
323
+ // THEN
324
+ expect(mockSignerSpy).toHaveBeenCalledWith(mockDerivationPath, {
325
+ transactions: [
326
+ "namespace-transaction-data",
327
+ "party-to-key-transaction-data",
328
+ "party-to-participant-transaction-data",
329
+ ],
330
+ });
331
+ });
332
+
333
+ it("should call signer with correct parameters for untyped versioned message with challenge", async () => {
334
+ const mockSignerSpy = jest.fn().mockResolvedValue({
335
+ signature: "main-signature",
336
+ applicationSignature: "challenge-signature",
337
+ });
338
+ const mockSignerWithSpy = {
339
+ ...mockSigner,
340
+ signTransaction: mockSignerSpy,
341
+ } as unknown as CantonSigner;
342
+
343
+ const mockOnboardingPrepareResponse: OnboardingPrepareResponse = {
344
+ party_id: "test-party-id",
345
+ party_name: "test-party-name",
346
+ public_key_fingerprint: "test-fingerprint",
347
+ challenge_nonce: "1234567890abcdef",
348
+ challenge_deadline: 1735689599,
349
+ transactions: {
350
+ namespace_transaction: {
351
+ serialized: "namespace-transaction-data",
352
+ json: {},
353
+ hash: "namespace-hash",
354
+ },
355
+ party_to_key_transaction: {
356
+ serialized: "party-to-key-transaction-data",
357
+ json: {},
358
+ hash: "party-to-key-hash",
359
+ },
360
+ party_to_participant_transaction: {
361
+ serialized: "party-to-participant-transaction-data",
362
+ json: {},
363
+ hash: "party-to-participant-hash",
364
+ },
365
+ combined_hash: "combined-hash",
366
+ },
367
+ };
368
+
369
+ const result = await signTransaction(
370
+ mockSignerWithSpy,
371
+ mockDerivationPath,
372
+ mockOnboardingPrepareResponse,
373
+ );
374
+
375
+ expect(result).toEqual({
376
+ signature: "main-signature",
377
+ applicationSignature: "challenge-signature",
378
+ });
379
+
380
+ expect(mockSignerSpy).toHaveBeenCalledWith(mockDerivationPath, {
381
+ transactions: [
382
+ "namespace-transaction-data",
383
+ "party-to-key-transaction-data",
384
+ "party-to-participant-transaction-data",
385
+ ],
386
+ challenge: "181234567890abcdef000000006774857f",
387
+ });
388
+ });
389
+ });
@@ -0,0 +1,59 @@
1
+ import { OnboardingPrepareResponse, PrepareTransferResponse } from "../../network/gateway";
2
+ import { PrepareTransactionResponse } from "../../types/onboard";
3
+ import { CantonSigner, CantonSignature } from "../../types/signer";
4
+ import { splitTransaction } from "./split";
5
+
6
+ /**
7
+ * Sign a Canton transaction - handles both prepared transactions and untyped versioned messages
8
+ */
9
+
10
+ export async function signTransaction(
11
+ signer: CantonSigner,
12
+ derivationPath: string,
13
+ transactionData: PrepareTransferResponse | PrepareTransactionResponse | OnboardingPrepareResponse,
14
+ ): Promise<CantonSignature> {
15
+ let signature: CantonSignature;
16
+
17
+ if ("json" in transactionData) {
18
+ const components = splitTransaction(transactionData.json);
19
+ signature = await signer.signTransaction(derivationPath, components);
20
+ } else {
21
+ const challenge = getTransactionChallenge(transactionData);
22
+
23
+ const transactions = [
24
+ transactionData.transactions.namespace_transaction.serialized,
25
+ transactionData.transactions.party_to_key_transaction.serialized,
26
+ transactionData.transactions.party_to_participant_transaction.serialized,
27
+ ];
28
+
29
+ signature = await signer.signTransaction(derivationPath, {
30
+ transactions,
31
+ ...(challenge && { challenge }),
32
+ });
33
+ }
34
+
35
+ if (!signature?.signature) {
36
+ throw new Error("Device returned empty signature");
37
+ }
38
+
39
+ return signature;
40
+ }
41
+
42
+ const getTransactionChallenge = ({
43
+ challenge_nonce,
44
+ challenge_deadline,
45
+ }: OnboardingPrepareResponse) => {
46
+ if (!challenge_nonce || !challenge_deadline) {
47
+ return undefined;
48
+ }
49
+
50
+ try {
51
+ const deadlineHex = Buffer.alloc(8);
52
+ deadlineHex.writeBigInt64BE(BigInt(challenge_deadline), 0);
53
+
54
+ // Format: [length (1B) = 0x18][challenge (16 bytes)][deadline (8 bytes)]
55
+ return "18" + challenge_nonce.toLowerCase() + deadlineHex.toString("hex");
56
+ } catch (error) {
57
+ return undefined;
58
+ }
59
+ };