@ledgerhq/coin-sui 0.11.0 → 0.12.0-nightly.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.
Files changed (187) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.unimportedrc.json +1 -0
  3. package/CHANGELOG.md +11 -0
  4. package/index.d.ts +0 -1
  5. package/jest.config.js +1 -1
  6. package/lib/bridge/broadcast.d.ts.map +1 -1
  7. package/lib/bridge/broadcast.js +3 -0
  8. package/lib/bridge/broadcast.js.map +1 -1
  9. package/lib/bridge/buildOptimisticOperation.js +31 -0
  10. package/lib/bridge/buildOptimisticOperation.js.map +1 -1
  11. package/lib/bridge/buildTransaction.d.ts +1 -1
  12. package/lib/bridge/buildTransaction.d.ts.map +1 -1
  13. package/lib/bridge/buildTransaction.js +3 -1
  14. package/lib/bridge/buildTransaction.js.map +1 -1
  15. package/lib/bridge/buildTransaction.test.js +4 -0
  16. package/lib/bridge/buildTransaction.test.js.map +1 -1
  17. package/lib/bridge/estimateMaxSpendable.d.ts.map +1 -1
  18. package/lib/bridge/estimateMaxSpendable.js +15 -3
  19. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  20. package/lib/bridge/estimateMaxSpendable.test.js +27 -0
  21. package/lib/bridge/estimateMaxSpendable.test.js.map +1 -1
  22. package/lib/bridge/getFeesForTransaction.d.ts.map +1 -1
  23. package/lib/bridge/getFeesForTransaction.js +13 -1
  24. package/lib/bridge/getFeesForTransaction.js.map +1 -1
  25. package/lib/bridge/getOperationExtra.d.ts +2 -0
  26. package/lib/bridge/getOperationExtra.d.ts.map +1 -0
  27. package/lib/bridge/getOperationExtra.js +6 -0
  28. package/lib/bridge/getOperationExtra.js.map +1 -0
  29. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  30. package/lib/bridge/getTransactionStatus.js +45 -20
  31. package/lib/bridge/getTransactionStatus.js.map +1 -1
  32. package/lib/bridge/preload.d.ts.map +1 -1
  33. package/lib/bridge/preload.js +5 -3
  34. package/lib/bridge/preload.js.map +1 -1
  35. package/lib/bridge/preload.test.js +13 -217
  36. package/lib/bridge/preload.test.js.map +1 -1
  37. package/lib/bridge/prepareTransaction.js +1 -1
  38. package/lib/bridge/prepareTransaction.js.map +1 -1
  39. package/lib/bridge/synchronisation.d.ts.map +1 -1
  40. package/lib/bridge/synchronisation.js +5 -2
  41. package/lib/bridge/synchronisation.js.map +1 -1
  42. package/lib/bridge/synchronisation.test.js +355 -7
  43. package/lib/bridge/synchronisation.test.js.map +1 -1
  44. package/lib/bridge/utils.d.ts +1 -1
  45. package/lib/bridge/utils.d.ts.map +1 -1
  46. package/lib/bridge/utils.js +22 -0
  47. package/lib/bridge/utils.js.map +1 -1
  48. package/lib/constants.d.ts +4 -0
  49. package/lib/constants.d.ts.map +1 -0
  50. package/lib/constants.js +7 -0
  51. package/lib/constants.js.map +1 -0
  52. package/lib/errors.d.ts +13 -0
  53. package/lib/errors.d.ts.map +1 -0
  54. package/lib/errors.js +21 -0
  55. package/lib/errors.js.map +1 -0
  56. package/lib/logic/craftTransaction.d.ts +5 -10
  57. package/lib/logic/craftTransaction.d.ts.map +1 -1
  58. package/lib/logic/craftTransaction.js +2 -1
  59. package/lib/logic/craftTransaction.js.map +1 -1
  60. package/lib/logic/estimateFees.d.ts +1 -1
  61. package/lib/logic/estimateFees.d.ts.map +1 -1
  62. package/lib/logic/estimateFees.js +14 -2
  63. package/lib/logic/estimateFees.js.map +1 -1
  64. package/lib/logic/index.d.ts +2 -1
  65. package/lib/logic/index.d.ts.map +1 -1
  66. package/lib/logic/index.js +3 -1
  67. package/lib/logic/index.js.map +1 -1
  68. package/lib/logic/stake.d.ts +3 -0
  69. package/lib/logic/stake.d.ts.map +1 -0
  70. package/lib/logic/stake.js +12 -0
  71. package/lib/logic/stake.js.map +1 -0
  72. package/lib/network/index.d.ts +5 -4
  73. package/lib/network/index.d.ts.map +1 -1
  74. package/lib/network/index.js +5 -3
  75. package/lib/network/index.js.map +1 -1
  76. package/lib/network/sdk.d.ts +5 -3
  77. package/lib/network/sdk.d.ts.map +1 -1
  78. package/lib/network/sdk.js +148 -26
  79. package/lib/network/sdk.js.map +1 -1
  80. package/lib/network/sdk.test.js +491 -6
  81. package/lib/network/sdk.test.js.map +1 -1
  82. package/lib/types/bridge.d.ts +33 -6
  83. package/lib/types/bridge.d.ts.map +1 -1
  84. package/lib-es/bridge/broadcast.d.ts.map +1 -1
  85. package/lib-es/bridge/broadcast.js +3 -0
  86. package/lib-es/bridge/broadcast.js.map +1 -1
  87. package/lib-es/bridge/buildOptimisticOperation.js +31 -0
  88. package/lib-es/bridge/buildOptimisticOperation.js.map +1 -1
  89. package/lib-es/bridge/buildTransaction.d.ts +1 -1
  90. package/lib-es/bridge/buildTransaction.d.ts.map +1 -1
  91. package/lib-es/bridge/buildTransaction.js +3 -1
  92. package/lib-es/bridge/buildTransaction.js.map +1 -1
  93. package/lib-es/bridge/buildTransaction.test.js +4 -0
  94. package/lib-es/bridge/buildTransaction.test.js.map +1 -1
  95. package/lib-es/bridge/estimateMaxSpendable.d.ts.map +1 -1
  96. package/lib-es/bridge/estimateMaxSpendable.js +15 -3
  97. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  98. package/lib-es/bridge/estimateMaxSpendable.test.js +27 -0
  99. package/lib-es/bridge/estimateMaxSpendable.test.js.map +1 -1
  100. package/lib-es/bridge/getFeesForTransaction.d.ts.map +1 -1
  101. package/lib-es/bridge/getFeesForTransaction.js +13 -1
  102. package/lib-es/bridge/getFeesForTransaction.js.map +1 -1
  103. package/lib-es/bridge/getOperationExtra.d.ts +2 -0
  104. package/lib-es/bridge/getOperationExtra.d.ts.map +1 -0
  105. package/lib-es/bridge/getOperationExtra.js +2 -0
  106. package/lib-es/bridge/getOperationExtra.js.map +1 -0
  107. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  108. package/lib-es/bridge/getTransactionStatus.js +46 -21
  109. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  110. package/lib-es/bridge/preload.d.ts.map +1 -1
  111. package/lib-es/bridge/preload.js +5 -3
  112. package/lib-es/bridge/preload.js.map +1 -1
  113. package/lib-es/bridge/preload.test.js +14 -218
  114. package/lib-es/bridge/preload.test.js.map +1 -1
  115. package/lib-es/bridge/prepareTransaction.js +1 -1
  116. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  117. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  118. package/lib-es/bridge/synchronisation.js +6 -3
  119. package/lib-es/bridge/synchronisation.js.map +1 -1
  120. package/lib-es/bridge/synchronisation.test.js +332 -7
  121. package/lib-es/bridge/synchronisation.test.js.map +1 -1
  122. package/lib-es/bridge/utils.d.ts +1 -1
  123. package/lib-es/bridge/utils.d.ts.map +1 -1
  124. package/lib-es/bridge/utils.js +22 -0
  125. package/lib-es/bridge/utils.js.map +1 -1
  126. package/lib-es/constants.d.ts +4 -0
  127. package/lib-es/constants.d.ts.map +1 -0
  128. package/lib-es/constants.js +4 -0
  129. package/lib-es/constants.js.map +1 -0
  130. package/lib-es/errors.d.ts +13 -0
  131. package/lib-es/errors.d.ts.map +1 -0
  132. package/lib-es/errors.js +18 -0
  133. package/lib-es/errors.js.map +1 -0
  134. package/lib-es/logic/craftTransaction.d.ts +5 -10
  135. package/lib-es/logic/craftTransaction.d.ts.map +1 -1
  136. package/lib-es/logic/craftTransaction.js +2 -1
  137. package/lib-es/logic/craftTransaction.js.map +1 -1
  138. package/lib-es/logic/estimateFees.d.ts +1 -1
  139. package/lib-es/logic/estimateFees.d.ts.map +1 -1
  140. package/lib-es/logic/estimateFees.js +14 -2
  141. package/lib-es/logic/estimateFees.js.map +1 -1
  142. package/lib-es/logic/index.d.ts +2 -1
  143. package/lib-es/logic/index.d.ts.map +1 -1
  144. package/lib-es/logic/index.js +1 -0
  145. package/lib-es/logic/index.js.map +1 -1
  146. package/lib-es/logic/stake.d.ts +3 -0
  147. package/lib-es/logic/stake.d.ts.map +1 -0
  148. package/lib-es/logic/stake.js +8 -0
  149. package/lib-es/logic/stake.js.map +1 -0
  150. package/lib-es/network/index.d.ts +5 -4
  151. package/lib-es/network/index.d.ts.map +1 -1
  152. package/lib-es/network/index.js +4 -3
  153. package/lib-es/network/index.js.map +1 -1
  154. package/lib-es/network/sdk.d.ts +5 -3
  155. package/lib-es/network/sdk.d.ts.map +1 -1
  156. package/lib-es/network/sdk.js +144 -25
  157. package/lib-es/network/sdk.js.map +1 -1
  158. package/lib-es/network/sdk.test.js +491 -6
  159. package/lib-es/network/sdk.test.js.map +1 -1
  160. package/lib-es/types/bridge.d.ts +33 -6
  161. package/lib-es/types/bridge.d.ts.map +1 -1
  162. package/package.json +17 -9
  163. package/src/bridge/broadcast.ts +3 -0
  164. package/src/bridge/buildOptimisticOperation.ts +47 -4
  165. package/src/bridge/buildTransaction.test.ts +4 -0
  166. package/src/bridge/buildTransaction.ts +3 -1
  167. package/src/bridge/estimateMaxSpendable.test.ts +33 -0
  168. package/src/bridge/estimateMaxSpendable.ts +17 -3
  169. package/src/bridge/getFeesForTransaction.ts +14 -1
  170. package/src/bridge/getOperationExtra.ts +1 -0
  171. package/src/bridge/getTransactionStatus.ts +53 -21
  172. package/src/bridge/preload.test.ts +13 -279
  173. package/src/bridge/preload.ts +5 -3
  174. package/src/bridge/prepareTransaction.ts +1 -1
  175. package/src/bridge/synchronisation.test.ts +389 -7
  176. package/src/bridge/synchronisation.ts +6 -3
  177. package/src/bridge/utils.ts +25 -1
  178. package/src/constants.ts +4 -0
  179. package/src/errors.ts +21 -0
  180. package/src/logic/craftTransaction.ts +6 -9
  181. package/src/logic/estimateFees.ts +16 -1
  182. package/src/logic/index.ts +2 -1
  183. package/src/logic/stake.ts +9 -0
  184. package/src/network/index.ts +6 -3
  185. package/src/network/sdk.test.ts +538 -10
  186. package/src/network/sdk.ts +179 -31
  187. package/src/types/bridge.ts +32 -6
@@ -4,6 +4,9 @@ import { faker } from "@faker-js/faker";
4
4
  import { createFixtureAccount, createFixtureOperation } from "../types/bridge.fixture";
5
5
  import { DEFAULT_COIN_TYPE } from "../network/sdk";
6
6
  import { getAccountShape } from "./synchronisation";
7
+ import coinConfig from "../config";
8
+ import { getFullnodeUrl } from "@mysten/sui/client";
9
+ import * as networkModule from "../network";
7
10
 
8
11
  // Mock getTokenById and listTokensForCryptoCurrency
9
12
  jest.mock("@ledgerhq/cryptoassets/tokens", () => ({
@@ -20,17 +23,34 @@ jest.mock("@ledgerhq/cryptoassets/tokens", () => ({
20
23
  listTokensForCryptoCurrency: () => [{ id: "0x123::sui::TEST" }],
21
24
  }));
22
25
 
23
- const mockGetAccountBalances = jest.fn();
24
- const mockGetOperations = jest.fn();
25
- jest.mock("../network", () => ({
26
- getAccountBalances: () => mockGetAccountBalances(),
27
- getOperations: () => mockGetOperations(),
28
- }));
26
+ jest.mock("../network", () => {
27
+ const mockGetAccountBalances = jest.fn();
28
+ const mockGetOperations = jest.fn();
29
+ const mockGetStakesRaw = jest.fn();
30
+ return {
31
+ getAccountBalances: mockGetAccountBalances,
32
+ getOperations: mockGetOperations,
33
+ getStakesRaw: mockGetStakesRaw,
34
+ createTransaction: jest.fn(),
35
+ };
36
+ });
29
37
 
30
38
  describe("getAccountShape", () => {
39
+ const mockGetAccountBalances = networkModule.getAccountBalances as jest.Mock;
40
+ const mockGetOperations = networkModule.getOperations as jest.Mock;
41
+ const mockGetStakesRaw = networkModule.getStakesRaw as jest.Mock;
42
+
31
43
  beforeEach(() => {
32
44
  mockGetAccountBalances.mockClear();
33
45
  mockGetOperations.mockClear();
46
+ mockGetStakesRaw.mockClear();
47
+ });
48
+
49
+ beforeAll(() => {
50
+ coinConfig.setCoinConfig(() => ({
51
+ status: { type: "active" },
52
+ node: { url: getFullnodeUrl("mainnet") },
53
+ }));
34
54
  });
35
55
 
36
56
  it("calls getAccountBalances and getOperations", async () => {
@@ -38,6 +58,7 @@ describe("getAccountShape", () => {
38
58
  const initialAccount = undefined;
39
59
  mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
40
60
  mockGetOperations.mockResolvedValue([]);
61
+ mockGetStakesRaw.mockResolvedValue([]);
41
62
 
42
63
  // WHEN
43
64
  await getAccountShape(
@@ -63,6 +84,7 @@ describe("getAccountShape", () => {
63
84
  const accountBalance = createAccountBalance();
64
85
  mockGetAccountBalances.mockResolvedValue([accountBalance]);
65
86
  mockGetOperations.mockResolvedValue([]);
87
+ mockGetStakesRaw.mockResolvedValue([]);
66
88
 
67
89
  // WHEN
68
90
  const shape = await getAccountShape(
@@ -85,8 +107,11 @@ describe("getAccountShape", () => {
85
107
  blockHeight: 5,
86
108
  operations: [],
87
109
  operationsCount: 0,
88
- suiResources: {},
110
+ suiResources: {
111
+ stakes: [],
112
+ },
89
113
  subAccounts: [],
114
+ syncHash: undefined,
90
115
  });
91
116
  });
92
117
 
@@ -98,6 +123,7 @@ describe("getAccountShape", () => {
98
123
  const accountBalance = createAccountBalance();
99
124
  mockGetAccountBalances.mockResolvedValue([accountBalance]);
100
125
  mockGetOperations.mockResolvedValue([]);
126
+ mockGetStakesRaw.mockResolvedValue([]);
101
127
 
102
128
  // WHEN
103
129
  const shape = await getAccountShape(
@@ -128,6 +154,7 @@ describe("getAccountShape", () => {
128
154
  createFixtureOperation({ id: faker.string.uuid(), extra }),
129
155
  ];
130
156
  mockGetOperations.mockResolvedValue(apiOperations);
157
+ mockGetStakesRaw.mockResolvedValue([]);
131
158
 
132
159
  // WHEN
133
160
  const shape = await getAccountShape(
@@ -154,6 +181,7 @@ describe("getAccountShape", () => {
154
181
  const tokenBalance = createAccountBalance({ coinType: "0x123::sui::TEST" });
155
182
  mockGetAccountBalances.mockResolvedValue([mainBalance, tokenBalance]);
156
183
  mockGetOperations.mockResolvedValue([]);
184
+ mockGetStakesRaw.mockResolvedValue([]);
157
185
 
158
186
  // WHEN
159
187
  const shape = await getAccountShape(
@@ -173,6 +201,360 @@ describe("getAccountShape", () => {
173
201
  expect(shape.subAccounts).toBeDefined();
174
202
  expect(Array.isArray(shape.subAccounts)).toBe(true);
175
203
  });
204
+
205
+ describe("stakes functionality", () => {
206
+ it("calls getStakesRaw with the correct address", async () => {
207
+ // GIVEN
208
+ const address = "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0";
209
+ const initialAccount = undefined;
210
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
211
+ mockGetOperations.mockResolvedValue([]);
212
+ mockGetStakesRaw.mockResolvedValue([]);
213
+
214
+ // WHEN
215
+ await getAccountShape(
216
+ {
217
+ index: 0,
218
+ derivationPath: "44'/784'/0'/0'/0'",
219
+ currency: getCryptoCurrencyById("sui"),
220
+ address,
221
+ initialAccount,
222
+ derivationMode: "sui",
223
+ },
224
+ { blacklistedTokenIds: [], paginationConfig: {} },
225
+ );
226
+
227
+ // THEN
228
+ expect(mockGetStakesRaw).toHaveBeenCalledTimes(1);
229
+ expect(mockGetStakesRaw).toHaveBeenCalledWith(address);
230
+ });
231
+
232
+ it("includes empty stakes in suiResources when no stakes are returned", async () => {
233
+ // GIVEN
234
+ const initialAccount = undefined;
235
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
236
+ mockGetOperations.mockResolvedValue([]);
237
+ mockGetStakesRaw.mockResolvedValue([]);
238
+
239
+ // WHEN
240
+ const shape = await getAccountShape(
241
+ {
242
+ index: 0,
243
+ derivationPath: "44'/784'/0'/0'/0'",
244
+ currency: getCryptoCurrencyById("sui"),
245
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
246
+ initialAccount,
247
+ derivationMode: "sui",
248
+ },
249
+ { blacklistedTokenIds: [], paginationConfig: {} },
250
+ );
251
+
252
+ // THEN
253
+ expect(shape.suiResources).toBeDefined();
254
+ expect(shape.suiResources?.stakes).toEqual([]);
255
+ });
256
+
257
+ it("includes stakes in suiResources when stakes are returned", async () => {
258
+ // GIVEN
259
+ const initialAccount = undefined;
260
+ const mockStakes = [
261
+ {
262
+ validatorAddress: "0xvalidator1",
263
+ stakes: [
264
+ {
265
+ stakedSuiId: "0xstake1",
266
+ status: "Active" as const,
267
+ principal: "1000000000",
268
+ stakeActiveEpoch: "100",
269
+ stakeRequestEpoch: "95",
270
+ },
271
+ ],
272
+ },
273
+ {
274
+ validatorAddress: "0xvalidator2",
275
+ stakes: [
276
+ {
277
+ stakedSuiId: "0xstake2",
278
+ status: "Pending" as const,
279
+ principal: "2000000000",
280
+ stakeActiveEpoch: "0",
281
+ stakeRequestEpoch: "100",
282
+ },
283
+ ],
284
+ },
285
+ ];
286
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
287
+ mockGetOperations.mockResolvedValue([]);
288
+ mockGetStakesRaw.mockResolvedValue(mockStakes);
289
+
290
+ // WHEN
291
+ const shape = await getAccountShape(
292
+ {
293
+ index: 0,
294
+ derivationPath: "44'/784'/0'/0'/0'",
295
+ currency: getCryptoCurrencyById("sui"),
296
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
297
+ initialAccount,
298
+ derivationMode: "sui",
299
+ },
300
+ { blacklistedTokenIds: [], paginationConfig: {} },
301
+ );
302
+
303
+ // THEN
304
+ expect(shape.suiResources).toBeDefined();
305
+ expect(shape.suiResources?.stakes).toEqual(mockStakes);
306
+ });
307
+
308
+ it("handles multiple stakes per validator", async () => {
309
+ // GIVEN
310
+ const initialAccount = undefined;
311
+ const mockStakes = [
312
+ {
313
+ validatorAddress: "0xvalidator1",
314
+ stakes: [
315
+ {
316
+ stakedSuiId: "0xstake1",
317
+ status: "Active" as const,
318
+ principal: "1000000000",
319
+ stakeActiveEpoch: "100",
320
+ stakeRequestEpoch: "95",
321
+ },
322
+ {
323
+ stakedSuiId: "0xstake2",
324
+ status: "Active" as const,
325
+ principal: "1500000000",
326
+ stakeActiveEpoch: "100",
327
+ stakeRequestEpoch: "95",
328
+ },
329
+ ],
330
+ },
331
+ ];
332
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
333
+ mockGetOperations.mockResolvedValue([]);
334
+ mockGetStakesRaw.mockResolvedValue(mockStakes);
335
+
336
+ // WHEN
337
+ const shape = await getAccountShape(
338
+ {
339
+ index: 0,
340
+ derivationPath: "44'/784'/0'/0'/0'",
341
+ currency: getCryptoCurrencyById("sui"),
342
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
343
+ initialAccount,
344
+ derivationMode: "sui",
345
+ },
346
+ { blacklistedTokenIds: [], paginationConfig: {} },
347
+ );
348
+
349
+ // THEN
350
+ expect(shape.suiResources?.stakes).toEqual(mockStakes);
351
+ expect(shape.suiResources?.stakes?.[0].stakes).toHaveLength(2);
352
+ });
353
+
354
+ it("handles different stake statuses correctly", async () => {
355
+ // GIVEN
356
+ const initialAccount = undefined;
357
+ const mockStakes = [
358
+ {
359
+ validatorAddress: "0xvalidator1",
360
+ stakes: [
361
+ {
362
+ stakedSuiId: "0xactive",
363
+ status: "Active" as const,
364
+ principal: "1000000000",
365
+ stakeActiveEpoch: "100",
366
+ stakeRequestEpoch: "95",
367
+ },
368
+ {
369
+ stakedSuiId: "0xpending",
370
+ status: "Pending" as const,
371
+ principal: "2000000000",
372
+ stakeActiveEpoch: "0",
373
+ stakeRequestEpoch: "100",
374
+ },
375
+ {
376
+ stakedSuiId: "0xunstaked",
377
+ status: "Unstaked" as const,
378
+ principal: "3000000000",
379
+ stakeActiveEpoch: "0",
380
+ stakeRequestEpoch: "0",
381
+ },
382
+ ],
383
+ },
384
+ ];
385
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
386
+ mockGetOperations.mockResolvedValue([]);
387
+ mockGetStakesRaw.mockResolvedValue(mockStakes);
388
+
389
+ // WHEN
390
+ const shape = await getAccountShape(
391
+ {
392
+ index: 0,
393
+ derivationPath: "44'/784'/0'/0'/0'",
394
+ currency: getCryptoCurrencyById("sui"),
395
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
396
+ initialAccount,
397
+ derivationMode: "sui",
398
+ },
399
+ { blacklistedTokenIds: [], paginationConfig: {} },
400
+ );
401
+
402
+ // THEN
403
+ expect(shape.suiResources?.stakes).toEqual(mockStakes);
404
+ const stakes = shape.suiResources?.stakes?.[0].stakes || [];
405
+ expect(stakes).toHaveLength(3);
406
+ expect(stakes.find(s => s.stakedSuiId === "0xactive")?.status).toBe("Active");
407
+ expect(stakes.find(s => s.stakedSuiId === "0xpending")?.status).toBe("Pending");
408
+ expect(stakes.find(s => s.stakedSuiId === "0xunstaked")?.status).toBe("Unstaked");
409
+ });
410
+
411
+ it("handles getStakesRaw throwing an error gracefully", async () => {
412
+ // GIVEN
413
+ const initialAccount = undefined;
414
+ const error = new Error("Network error");
415
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
416
+ mockGetOperations.mockResolvedValue([]);
417
+ mockGetStakesRaw.mockRejectedValue(error);
418
+
419
+ // WHEN & THEN
420
+ await expect(
421
+ getAccountShape(
422
+ {
423
+ index: 0,
424
+ derivationPath: "44'/784'/0'/0'/0'",
425
+ currency: getCryptoCurrencyById("sui"),
426
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
427
+ initialAccount,
428
+ derivationMode: "sui",
429
+ },
430
+ { blacklistedTokenIds: [], paginationConfig: {} },
431
+ ),
432
+ ).rejects.toThrow("Network error");
433
+ });
434
+
435
+ it("preserves existing suiResources when initialAccount has stakes", async () => {
436
+ // GIVEN
437
+ const existingStakes = [
438
+ {
439
+ validatorAddress: "0xexistingValidator",
440
+ stakes: [
441
+ {
442
+ stakedSuiId: "0xexistingStake",
443
+ status: "Active" as const,
444
+ principal: "500000000",
445
+ stakeActiveEpoch: "90",
446
+ stakeRequestEpoch: "85",
447
+ },
448
+ ],
449
+ },
450
+ ];
451
+ const initialAccount = createFixtureAccount({
452
+ suiResources: { stakes: existingStakes },
453
+ });
454
+ const newStakes = [
455
+ {
456
+ validatorAddress: "0xnewValidator",
457
+ stakes: [
458
+ {
459
+ stakedSuiId: "0xnewStake",
460
+ status: "Active" as const,
461
+ principal: "1000000000",
462
+ stakeActiveEpoch: "100",
463
+ stakeRequestEpoch: "95",
464
+ },
465
+ ],
466
+ },
467
+ ];
468
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
469
+ mockGetOperations.mockResolvedValue([]);
470
+ mockGetStakesRaw.mockResolvedValue(newStakes);
471
+
472
+ // WHEN
473
+ const shape = await getAccountShape(
474
+ {
475
+ index: 0,
476
+ derivationPath: "44'/784'/0'/0'/0'",
477
+ currency: getCryptoCurrencyById("sui"),
478
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
479
+ initialAccount,
480
+ derivationMode: "sui",
481
+ },
482
+ { blacklistedTokenIds: [], paginationConfig: {} },
483
+ );
484
+
485
+ // THEN
486
+ expect(shape.suiResources?.stakes).toEqual(newStakes);
487
+ // The new stakes should replace the old ones, not merge
488
+ expect(shape.suiResources?.stakes).not.toEqual(existingStakes);
489
+ });
490
+
491
+ it("handles large stake amounts correctly", async () => {
492
+ // GIVEN
493
+ const initialAccount = undefined;
494
+ const mockStakes = [
495
+ {
496
+ validatorAddress: "0xvalidator1",
497
+ stakes: [
498
+ {
499
+ stakedSuiId: "0xlargeStake",
500
+ status: "Active" as const,
501
+ principal: "999999999999999999999999999999",
502
+ stakeActiveEpoch: "1000",
503
+ stakeRequestEpoch: "995",
504
+ },
505
+ ],
506
+ },
507
+ ];
508
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
509
+ mockGetOperations.mockResolvedValue([]);
510
+ mockGetStakesRaw.mockResolvedValue(mockStakes);
511
+
512
+ // WHEN
513
+ const shape = await getAccountShape(
514
+ {
515
+ index: 0,
516
+ derivationPath: "44'/784'/0'/0'/0'",
517
+ currency: getCryptoCurrencyById("sui"),
518
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
519
+ initialAccount,
520
+ derivationMode: "sui",
521
+ },
522
+ { blacklistedTokenIds: [], paginationConfig: {} },
523
+ );
524
+
525
+ // THEN
526
+ expect(shape.suiResources?.stakes).toEqual(mockStakes);
527
+ const stake = shape.suiResources?.stakes?.[0].stakes?.[0];
528
+ expect(stake?.principal).toBe("999999999999999999999999999999");
529
+ expect(stake?.stakeActiveEpoch).toBe("1000");
530
+ expect(stake?.stakeRequestEpoch).toBe("995");
531
+ });
532
+
533
+ it("handles getStakesRaw returning null or undefined", async () => {
534
+ // GIVEN
535
+ const initialAccount = undefined;
536
+ mockGetAccountBalances.mockResolvedValue([createAccountBalance()]);
537
+ mockGetOperations.mockResolvedValue([]);
538
+ mockGetStakesRaw.mockResolvedValue(null);
539
+
540
+ // WHEN
541
+ const shape = await getAccountShape(
542
+ {
543
+ index: 0,
544
+ derivationPath: "44'/784'/0'/0'/0'",
545
+ currency: getCryptoCurrencyById("sui"),
546
+ address: "0x6e143fe0a8ca010a86580dafac44298e5b1b7d73efc345356a59a15f0d7824f0",
547
+ initialAccount,
548
+ derivationMode: "sui",
549
+ },
550
+ { blacklistedTokenIds: [], paginationConfig: {} },
551
+ );
552
+
553
+ // THEN
554
+ expect(shape.suiResources).toBeDefined();
555
+ expect(shape.suiResources?.stakes).toBeNull();
556
+ });
557
+ });
176
558
  });
177
559
 
178
560
  function createAccountBalance(overrides = {}) {
@@ -11,7 +11,7 @@ import {
11
11
  } from "@ledgerhq/coin-framework/bridge/jsHelpers";
12
12
  import { type Operation } from "@ledgerhq/types-live";
13
13
  import { listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/tokens";
14
- import { getAccountBalances, getOperations } from "../network";
14
+ import { getAccountBalances, getOperations, getStakesRaw } from "../network";
15
15
  import { DEFAULT_COIN_TYPE } from "../network/sdk";
16
16
  import { SuiOperationExtra, SuiAccount } from "../types";
17
17
  import type { SyncConfig, TokenAccount } from "@ledgerhq/types-live";
@@ -41,6 +41,7 @@ export const getAccountShape: GetAccountShape<SuiAccount> = async (info, syncCon
41
41
  });
42
42
 
43
43
  let operations: Operation[] = [];
44
+ const stakes = await getStakesRaw(address);
44
45
 
45
46
  let syncHash = initialAccount?.syncHash ?? latestHash(oldOperations);
46
47
  const newOperations = await getOperations(accountId, address, syncHash);
@@ -84,7 +85,9 @@ export const getAccountShape: GetAccountShape<SuiAccount> = async (info, syncCon
84
85
  operationsCount: mainAccountOperations.length,
85
86
  blockHeight: 5,
86
87
  subAccounts,
87
- suiResources: {},
88
+ suiResources: {
89
+ stakes,
90
+ },
88
91
  operations: mainAccountOperations,
89
92
  };
90
93
  };
@@ -118,7 +121,7 @@ async function buildSubAccounts({
118
121
  const existingAccountByTicker: { [ticker: string]: TokenAccount } = {}; // used for fast lookup
119
122
  const existingAccountTickers: string[] = []; // used to keep track of ordering
120
123
 
121
- if (initialAccount && initialAccount.subAccounts) {
124
+ if (initialAccount?.subAccounts) {
122
125
  for (const existingSubAccount of initialAccount.subAccounts) {
123
126
  if (existingSubAccount.type === "TokenAccount") {
124
127
  const { ticker, id } = existingSubAccount.token;
@@ -35,6 +35,22 @@ const calculateMaxSend = (account: SuiAccount, transaction: Transaction): BigNum
35
35
  return amount.lt(0) ? new BigNumber(0) : amount;
36
36
  };
37
37
 
38
+ /**
39
+ * Calculate the maximum amount that can be delegated after deducting fees and reserving gas.
40
+ *
41
+ * @param {SuiAccount} account - The account from which the amount is being delegated.
42
+ * @param {Transaction} transaction - The transaction details including fees.
43
+ * @returns {BigNumber} - The maximum amount that can be delegated, or 0 if insufficient balance.
44
+ */
45
+ const calculateMaxDelegate = (account: SuiAccount, transaction: Transaction): BigNumber => {
46
+ // Reserve 0.1 SUI for future gas fees as recommended for delegation
47
+ const ONE_SUI = new BigNumber("1000000000"); // 1 SUI in MIST
48
+ const gasReserve = ONE_SUI.div(10); // 0.1 SUI
49
+
50
+ const amount = account.spendableBalance.minus(transaction.fees || 0).minus(gasReserve);
51
+ return amount.lt(0) ? new BigNumber(0) : amount;
52
+ };
53
+
38
54
  /**
39
55
  * Calculates the amount to be sent in a transaction based on the account's balance and transaction details.
40
56
  *
@@ -61,6 +77,14 @@ export const calculateAmount = ({
61
77
  amount =
62
78
  findSubAccountById(account, transaction.subAccountId!)?.spendableBalance ??
63
79
  new BigNumber(0);
80
+ break;
81
+ case "delegate":
82
+ amount = calculateMaxDelegate(account, transaction);
83
+ break;
84
+ case "undelegate":
85
+ // For undelegate, use the full staked amount (handled elsewhere)
86
+ amount = transaction.amount;
87
+ break;
64
88
  }
65
89
  } else if (transaction.amount.gt(MAX_AMOUNT_INPUT)) {
66
90
  return new BigNumber(MAX_AMOUNT_INPUT);
@@ -69,6 +93,6 @@ export const calculateAmount = ({
69
93
  return amount.lt(0) ? new BigNumber(0) : amount;
70
94
  };
71
95
 
72
- export const assertUnreachable = (_: never): never => {
96
+ export const assertUnreachable = (_: unknown): never => {
73
97
  throw new Error("unreachable assertion failed");
74
98
  };
@@ -0,0 +1,4 @@
1
+ export const STAKING_GAS_BASE = "25000000000000";
2
+ export const FIGMENT_SUI_VALIDATOR_ADDRESS =
3
+ "0x3d9fb148e35ef4d74fcfc36995da14fc504b885d5f2bfeca37d6ea2cc044a32d";
4
+ export const ONE_SUI = 1000000000;
package/src/errors.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createCustomErrorClass } from "@ledgerhq/errors";
2
+
3
+ /*
4
+ * One SUI is minimal to stake.
5
+ */
6
+ export const OneSuiMinForStake = createCustomErrorClass("OneSuiMinForStake");
7
+
8
+ /*
9
+ * One SUI is minimal for partial unstake.
10
+ */
11
+ export const OneSuiMinForUnstake = createCustomErrorClass("OneSuiMinForUnstake");
12
+
13
+ /*
14
+ * One SUI is minimal to be left when partial unstake.
15
+ */
16
+ export const OneSuiMinForUnstakeToBeLeft = createCustomErrorClass("OneSuiMinForUnstakeToBeLeft");
17
+
18
+ /*
19
+ * At least 0.1 SUI to unstake
20
+ */
21
+ export const SomeSuiForUnstake = createCustomErrorClass("SomeSuiForUnstake");
@@ -4,21 +4,17 @@ import type { SuiTransactionMode, CoreTransaction } from "../types";
4
4
  import suiAPI from "../network";
5
5
  import { DEFAULT_COIN_TYPE } from "../network/sdk";
6
6
 
7
- export type CreateExtrinsicArg = {
8
- amount: BigNumber;
9
- coinType: string;
10
- mode: SuiTransactionMode;
11
- recipient: string;
12
- useAllAmount?: boolean | undefined;
13
- };
14
-
15
7
  export async function craftTransaction({
16
8
  amount,
17
9
  asset,
18
10
  recipient,
19
11
  sender,
20
12
  type,
21
- }: TransactionIntent): Promise<CoreTransaction> {
13
+ ...extra
14
+ }: TransactionIntent & {
15
+ useAllAmount?: boolean;
16
+ stakedSuiId?: string;
17
+ }): Promise<CoreTransaction> {
22
18
  let coinType = DEFAULT_COIN_TYPE;
23
19
  if (asset.type === "token" && asset.assetReference) {
24
20
  coinType = asset.assetReference;
@@ -28,6 +24,7 @@ export async function craftTransaction({
28
24
  coinType,
29
25
  mode: type as SuiTransactionMode,
30
26
  recipient,
27
+ ...extra,
31
28
  });
32
29
 
33
30
  return { unsigned };
@@ -8,13 +8,28 @@ export async function estimateFees({
8
8
  amount,
9
9
  sender,
10
10
  asset,
11
+ type,
11
12
  }: TransactionIntent): Promise<bigint> {
12
13
  let coinType = DEFAULT_COIN_TYPE;
13
14
  if (asset.type === "token" && asset.assetReference) {
14
15
  coinType = asset.assetReference;
15
16
  }
17
+
18
+ let mode: "send" | "delegate" | "undelegate";
19
+ switch (type) {
20
+ case "delegate":
21
+ mode = "delegate";
22
+ break;
23
+ case "undelegate":
24
+ mode = "undelegate";
25
+ break;
26
+ default:
27
+ mode = "send";
28
+ break;
29
+ }
30
+
16
31
  const { gasBudget } = await suiAPI.paymentInfo(sender, {
17
- mode: "send",
32
+ mode,
18
33
  family: "sui",
19
34
  recipient,
20
35
  amount: BigNumber(amount.toString()),
@@ -1,4 +1,4 @@
1
- export { craftTransaction, type CreateExtrinsicArg } from "./craftTransaction";
1
+ export { craftTransaction } from "./craftTransaction";
2
2
  export { estimateFees } from "./estimateFees";
3
3
  export { broadcast } from "./broadcast";
4
4
  export { combine } from "./combine";
@@ -7,3 +7,4 @@ export { lastBlock } from "./lastBlock";
7
7
  export { getBlock, getBlockInfo } from "./getBlock";
8
8
  export { listOperations } from "./listOperations";
9
9
  export { getStakes, getRewards } from "./staking";
10
+ export { canStake } from "./stake";
@@ -0,0 +1,9 @@
1
+ import { SuiAccount } from "../types";
2
+ import { ONE_SUI } from "../constants";
3
+
4
+ /*
5
+ * Make sure that an account has enough funds to stake, unstake, AND withdraw before staking.
6
+ */
7
+ export const canStake = (account: SuiAccount): boolean => {
8
+ return account.balance.gte(ONE_SUI);
9
+ };
@@ -4,10 +4,11 @@ import {
4
4
  getOperations,
5
5
  getBlock,
6
6
  getBlockInfo,
7
- getStakes,
7
+ getStakesRaw,
8
8
  paymentInfo,
9
9
  createTransaction,
10
10
  executeTransactionBlock,
11
+ getStakes,
11
12
  } from "./sdk";
12
13
 
13
14
  export {
@@ -16,10 +17,11 @@ export {
16
17
  getOperations,
17
18
  getBlock,
18
19
  getBlockInfo,
19
- getStakes,
20
+ getStakesRaw,
20
21
  paymentInfo,
21
22
  createTransaction,
22
23
  executeTransactionBlock,
24
+ getStakes,
23
25
  };
24
26
 
25
27
  export default {
@@ -28,8 +30,9 @@ export default {
28
30
  getOperations,
29
31
  getBlock,
30
32
  getBlockInfo,
31
- getStakes,
33
+ getStakesRaw,
32
34
  paymentInfo,
33
35
  createTransaction,
34
36
  executeTransactionBlock,
37
+ getStakes,
35
38
  };