@forevermoney/sdk 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,89 +1,5 @@
1
- // src/core/addresses.ts
2
- import {
3
- AccountId,
4
- Blake2256,
5
- getSs58AddressInfo
6
- } from "@polkadot-api/substrate-bindings";
7
- import {
8
- bytesToHex,
9
- concat,
10
- getAddress,
11
- hexToBytes,
12
- isAddress,
13
- stringToBytes,
14
- zeroAddress
15
- } from "viem";
16
-
17
- // src/core/errors.ts
18
- var ForeverMoneyError = class extends Error {
19
- code;
20
- details;
21
- constructor(code, message, details) {
22
- super(message);
23
- this.name = "ForeverMoneyError";
24
- this.code = code;
25
- this.details = details === void 0 ? void 0 : Object.freeze({ ...details });
26
- }
27
- };
28
-
29
- // src/core/addresses.ts
30
- var BITTENSOR_SS58_PREFIX = 42;
31
- function normalizeEvmAddress(value) {
32
- const candidate = typeof value === "string" && /^0x[0-9A-F]{40}$/.test(value) ? value.toLowerCase() : value;
33
- if (!isAddress(candidate) || candidate === zeroAddress) {
34
- throw new ForeverMoneyError(
35
- "INVALID_ADDRESS",
36
- "Expected a non-zero EVM address."
37
- );
38
- }
39
- return getAddress(candidate);
40
- }
41
- function evmToMirrorSS58(evmAddress) {
42
- return AccountId(BITTENSOR_SS58_PREFIX).dec(
43
- Blake2256(
44
- concat([
45
- stringToBytes("evm:"),
46
- hexToBytes(normalizeEvmAddress(evmAddress))
47
- ])
48
- )
49
- );
50
- }
51
- function decodeBittensorAddress(value) {
52
- try {
53
- if (typeof value !== "string" || !value || value.startsWith("0x"))
54
- throw new Error("Raw hex is not SS58.");
55
- const info = getSs58AddressInfo(value);
56
- if (!info.isValid || info.ss58Format !== BITTENSOR_SS58_PREFIX || info.publicKey.length !== 32)
57
- throw new Error("Invalid Bittensor address.");
58
- const publicKey = info.publicKey;
59
- return publicKey;
60
- } catch {
61
- throw new ForeverMoneyError(
62
- "INVALID_SS58",
63
- "Expected a Bittensor SS58 address with prefix 42."
64
- );
65
- }
66
- }
67
- function normalizeSS58(value) {
68
- return AccountId(BITTENSOR_SS58_PREFIX).dec(decodeBittensorAddress(value));
69
- }
70
- function isBittensorSS58(value) {
71
- try {
72
- decodeBittensorAddress(value);
73
- return true;
74
- } catch {
75
- return false;
76
- }
77
- }
78
- function ss58ToPublicKey(value) {
79
- return bytesToHex(decodeBittensorAddress(value));
80
- }
81
-
82
- // src/core/amounts.ts
83
- import { maxUint256, formatUnits, parseUnits } from "viem";
84
-
85
1
  // src/chains/deployment.ts
86
- import { getAddress as getAddress2 } from "viem";
2
+ import { getAddress } from "viem";
87
3
  var BASE_CHAIN_ID = 8453;
88
4
  var ROBINHOOD_CHAIN_ID = 4663;
89
5
  var SUBTENSOR_CHAIN_ID = 964;
@@ -102,34 +18,34 @@ var foreverMoneyDeployment = Object.freeze({
102
18
  chainId: BASE_CHAIN_ID,
103
19
  ccipSelector: BASE_CCIP_SELECTOR,
104
20
  contracts: Object.freeze({
105
- gateway: getAddress2("0x1da2415229b614C787e145D1D7346eb496319C52"),
21
+ gateway: getAddress("0x1da2415229b614C787e145D1D7346eb496319C52"),
106
22
  // Retired gateways, oldest first; kept only to track transfers sent
107
23
  // through them.
108
24
  legacyGateways: [
109
- getAddress2("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
25
+ getAddress("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
110
26
  ],
111
- wrappedTao: getAddress2(
27
+ wrappedTao: getAddress(
112
28
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
113
29
  ),
114
- wrappedSn80: getAddress2(
30
+ wrappedSn80: getAddress(
115
31
  "0x6f63d869011f95274498023b4abfc00b30c34378"
116
32
  ),
117
- ccipRouter: getAddress2(
33
+ ccipRouter: getAddress(
118
34
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
119
35
  ),
120
- ccipOffRampFromSubtensor: getAddress2(
36
+ ccipOffRampFromSubtensor: getAddress(
121
37
  "0xf09AFe78d3c7d359b334d7cB88995751F7eC5E13"
122
38
  ),
123
- vaultFactory: getAddress2(
39
+ vaultFactory: getAddress(
124
40
  "0x9b7F3c7aa335D8BF9f91741E541820466735F868"
125
41
  ),
126
- vaultManagerImplementation: getAddress2(
42
+ vaultManagerImplementation: getAddress(
127
43
  "0x235Ad8654fe3a1619a9892eBCaD9361F416168da"
128
44
  ),
129
- batchLiquidityManagerQuery: getAddress2(
45
+ batchLiquidityManagerQuery: getAddress(
130
46
  "0xd117176C5E6809b68500995F59589A8c081565A5"
131
47
  ),
132
- weth: getAddress2("0x4200000000000000000000000000000000000006")
48
+ weth: getAddress("0x4200000000000000000000000000000000000006")
133
49
  }),
134
50
  deploymentBlock: 50098800
135
51
  }),
@@ -139,17 +55,17 @@ var foreverMoneyDeployment = Object.freeze({
139
55
  chainId: ROBINHOOD_CHAIN_ID,
140
56
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
141
57
  contracts: Object.freeze({
142
- gateway: getAddress2("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
58
+ gateway: getAddress("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
143
59
  legacyGateways: [
144
- getAddress2("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
60
+ getAddress("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
145
61
  ],
146
- wrappedTao: getAddress2(
62
+ wrappedTao: getAddress(
147
63
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
148
64
  ),
149
- ccipRouter: getAddress2(
65
+ ccipRouter: getAddress(
150
66
  "0x06fC836cf9839B1cd891C440A0a45242DA6Ae1c9"
151
67
  ),
152
- ccipOffRampFromSubtensor: getAddress2(
68
+ ccipOffRampFromSubtensor: getAddress(
153
69
  "0xcDca5D374e46A6DDDab50bD2D9acB8c796eC35C3"
154
70
  )
155
71
  })
@@ -159,32 +75,32 @@ var foreverMoneyDeployment = Object.freeze({
159
75
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
160
76
  contracts: Object.freeze({
161
77
  // V5.1 AlphaGateway: multi-validator staked input (tao-bridge 485c898).
162
- gateway: getAddress2("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
78
+ gateway: getAddress("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
163
79
  // V4 and V5 hubs. The spokes still deliver to the V5 hub (their hub
164
80
  // pointer is immutable), so it stays here for delivery tracking.
165
81
  legacyGateways: [
166
- getAddress2("0x998f20Fea90bF7792774dECc7f994716442B1705"),
167
- getAddress2("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
82
+ getAddress("0x998f20Fea90bF7792774dECc7f994716442B1705"),
83
+ getAddress("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
168
84
  ],
169
- alphaVault: getAddress2(
85
+ alphaVault: getAddress(
170
86
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
171
87
  ),
172
- wrappedTao: getAddress2(
88
+ wrappedTao: getAddress(
173
89
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
174
90
  ),
175
- wrappedSn80: getAddress2(
91
+ wrappedSn80: getAddress(
176
92
  "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
177
93
  ),
178
- ccipRouter: getAddress2(
94
+ ccipRouter: getAddress(
179
95
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
180
96
  ),
181
- ccipOffRampFromBase: getAddress2(
97
+ ccipOffRampFromBase: getAddress(
182
98
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
183
99
  ),
184
- ccipOffRampFromRobinhood: getAddress2(
100
+ ccipOffRampFromRobinhood: getAddress(
185
101
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
186
102
  ),
187
- stakingPrecompile: getAddress2(
103
+ stakingPrecompile: getAddress(
188
104
  "0x0000000000000000000000000000000000000805"
189
105
  )
190
106
  })
@@ -194,6 +110,21 @@ function getForeverMoneyEvmDeployment(chain = "base") {
194
110
  return foreverMoneyDeployment[chain];
195
111
  }
196
112
 
113
+ // src/core/amounts.ts
114
+ import { maxUint256, formatUnits, parseUnits } from "viem";
115
+
116
+ // src/core/errors.ts
117
+ var ForeverMoneyError = class extends Error {
118
+ code;
119
+ details;
120
+ constructor(code, message, details) {
121
+ super(message);
122
+ this.name = "ForeverMoneyError";
123
+ this.code = code;
124
+ this.details = details === void 0 ? void 0 : Object.freeze({ ...details });
125
+ }
126
+ };
127
+
197
128
  // src/core/amounts.ts
198
129
  var BASIS_POINTS = 10000n;
199
130
  var NETWORK_FEE_BUFFER_BPS = 200n;
@@ -340,6 +271,289 @@ function gasLimitWithBuffer(gasLimit) {
340
271
  return buffered;
341
272
  }
342
273
 
274
+ // src/core/validation.ts
275
+ import { maxUint256 as maxUint2562, zeroHash, isHex } from "viem";
276
+ function assertBigInt(amount, label) {
277
+ if (typeof amount !== "bigint") {
278
+ throw new ForeverMoneyError(
279
+ "INVALID_TRANSACTION_PLAN",
280
+ `${label} must be a bigint.`
281
+ );
282
+ }
283
+ }
284
+ function assertPositiveAmount(amount, label) {
285
+ assertBigInt(amount, label);
286
+ if (amount === 0n) {
287
+ throw new ForeverMoneyError(
288
+ "AMOUNT_ZERO",
289
+ `${label} must be greater than zero.`
290
+ );
291
+ }
292
+ if (amount < 0n) {
293
+ throw new ForeverMoneyError(
294
+ "INVALID_TRANSACTION_PLAN",
295
+ `${label} cannot be negative.`
296
+ );
297
+ }
298
+ if (amount > maxUint2562) {
299
+ throw new ForeverMoneyError(
300
+ "INVALID_TRANSACTION_PLAN",
301
+ `${label} exceeds uint256.`
302
+ );
303
+ }
304
+ }
305
+ function assertNonNegativeAmount(amount, label) {
306
+ assertBigInt(amount, label);
307
+ if (amount < 0n) {
308
+ throw new ForeverMoneyError(
309
+ "INVALID_TRANSACTION_PLAN",
310
+ `${label} cannot be negative.`
311
+ );
312
+ }
313
+ if (amount > maxUint2562) {
314
+ throw new ForeverMoneyError(
315
+ "INVALID_TRANSACTION_PLAN",
316
+ `${label} exceeds uint256.`
317
+ );
318
+ }
319
+ }
320
+ function assertBoolean(value, label) {
321
+ if (typeof value !== "boolean") {
322
+ throw new ForeverMoneyError(
323
+ "INVALID_TRANSACTION_PLAN",
324
+ `${label} must be a boolean.`
325
+ );
326
+ }
327
+ }
328
+ function assertArray(value, label) {
329
+ if (!Array.isArray(value)) {
330
+ throw new ForeverMoneyError(
331
+ "INVALID_TRANSACTION_PLAN",
332
+ `${label} must be an array.`
333
+ );
334
+ }
335
+ }
336
+ function assertRecord(value, label) {
337
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
338
+ throw new ForeverMoneyError(
339
+ "INVALID_TRANSACTION_PLAN",
340
+ `${label} must be an object.`
341
+ );
342
+ }
343
+ }
344
+ function normalizeBytes32(value, label) {
345
+ if (!isHex(value, { strict: true }) || value.length !== 66) {
346
+ throw new ForeverMoneyError(
347
+ "INVALID_BYTES32",
348
+ `${label} must be a 32-byte hex value.`
349
+ );
350
+ }
351
+ return value.toLowerCase();
352
+ }
353
+ function normalizeOptionalBytes32(value, label) {
354
+ return value === void 0 ? zeroHash : normalizeBytes32(value, label);
355
+ }
356
+
357
+ // src/bridge/stake-rounding.ts
358
+ function invalid(message) {
359
+ throw new ForeverMoneyError("INVALID_TRANSACTION_PLAN", message);
360
+ }
361
+ function stakedMinimumOutput(amountWei, sourceCount = 1) {
362
+ assertWholeRao(amountWei);
363
+ if (!Number.isInteger(sourceCount) || sourceCount < 1 || sourceCount > 16)
364
+ invalid("Source count must be between 1 and 16.");
365
+ const rao = amountWei / EVM_WEI_PER_RAO;
366
+ const dust = 2n * BigInt(sourceCount) + 2n;
367
+ return (rao > dust ? rao - dust : rao) * EVM_WEI_PER_RAO;
368
+ }
369
+ function strandedStakeSource(error) {
370
+ const queue = [error];
371
+ const seen = /* @__PURE__ */ new Set();
372
+ while (queue.length && seen.size < 20) {
373
+ const entry = queue.shift();
374
+ if (typeof entry === "string" && /^0x5408a598[0-9a-f]{128}$/i.test(entry))
375
+ return `0x${entry.slice(10, 74)}`.toLowerCase();
376
+ if (entry && typeof entry === "object" && !seen.has(entry)) {
377
+ seen.add(entry);
378
+ for (const key of [
379
+ "data",
380
+ "error",
381
+ "info",
382
+ "cause",
383
+ "details",
384
+ "rpcData"
385
+ ]) {
386
+ const nested = entry[key];
387
+ if (nested != null) queue.push(nested);
388
+ }
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ async function estimateRoundedStake({
394
+ amountWei,
395
+ minAmountOutWei,
396
+ pulls,
397
+ positions,
398
+ minStakeRao = 0n,
399
+ adjustRounding = true,
400
+ partnerFeeBps = 0,
401
+ quoteAndEstimate,
402
+ isActive = () => true
403
+ }) {
404
+ assertWholeRao(amountWei);
405
+ assertPositiveAmount(minAmountOutWei, "Minimum output");
406
+ if (minAmountOutWei > amountWei)
407
+ invalid("Minimum output cannot exceed input.");
408
+ assertBoolean(adjustRounding, "adjustRounding");
409
+ assertNonNegativeAmount(minStakeRao, "Minimum remaining stake");
410
+ if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0 || partnerFeeBps > 1e4)
411
+ invalid("Invalid partner fee basis points.");
412
+ if (!Array.isArray(pulls) || pulls.length < 1 || pulls.length > 16)
413
+ invalid("Provide between 1 and 16 stake pulls.");
414
+ const keys = /* @__PURE__ */ new Set();
415
+ const originalPulls = pulls.map((pull) => {
416
+ const hotkey = normalizeBytes32(pull.hotkey, "Validator").toLowerCase();
417
+ if (keys.has(hotkey)) invalid("Duplicate stake source.");
418
+ keys.add(hotkey);
419
+ assertPositiveAmount(pull.amountRao, "Stake pull");
420
+ return { hotkey, amountRao: pull.amountRao };
421
+ });
422
+ if (originalPulls.reduce((sum, pull) => sum + pull.amountRao, 0n) * EVM_WEI_PER_RAO !== amountWei)
423
+ invalid("Stake pulls must sum to the input amount.");
424
+ const balances = /* @__PURE__ */ new Map();
425
+ if (!Array.isArray(positions)) invalid("Stake positions must be an array.");
426
+ for (const position of positions) {
427
+ const key = normalizeBytes32(position.hotkey, "Validator").toLowerCase();
428
+ if (balances.has(key)) invalid("Duplicate stake position.");
429
+ assertNonNegativeAmount(position.stakeRao, "Stake balance");
430
+ balances.set(key, position.stakeRao);
431
+ }
432
+ const fits = (candidatePulls2) => {
433
+ const total = candidatePulls2.reduce(
434
+ (sum, pull) => sum + pull.amountRao,
435
+ 0n
436
+ );
437
+ const cut = total * BigInt(partnerFeeBps) / 10000n;
438
+ const shares = candidatePulls2.map(
439
+ (pull, index) => index ? cut * pull.amountRao / total : 0n
440
+ );
441
+ shares[0] = cut - shares.reduce((sum, share) => sum + share, 0n);
442
+ return candidatePulls2.every((pull, index) => {
443
+ const balance = balances.get(pull.hotkey);
444
+ if (balance == null) return false;
445
+ const remainder = balance - pull.amountRao - shares[index];
446
+ return remainder === 0n || remainder > 0n && remainder >= minStakeRao;
447
+ });
448
+ };
449
+ if (!fits(originalPulls))
450
+ invalid(
451
+ "Stake positions cannot cover the pulls, partner fee, and minimum remainder."
452
+ );
453
+ let candidate = amountWei;
454
+ let candidatePulls = originalPulls;
455
+ for (let attempt = 0; ; attempt++) {
456
+ if (!isActive()) throw new Error("Stake quote cancelled.");
457
+ try {
458
+ const result = await quoteAndEstimate(
459
+ candidate,
460
+ candidatePulls,
461
+ minAmountOutWei
462
+ );
463
+ if (!isActive()) throw new Error("Stake quote cancelled.");
464
+ return Object.freeze({
465
+ ...result,
466
+ amountWei: candidate,
467
+ pulls: Object.freeze(
468
+ candidatePulls.map((pull) => Object.freeze({ ...pull }))
469
+ ),
470
+ minAmountOutWei
471
+ });
472
+ } catch (error) {
473
+ const hotkey = strandedStakeSource(error);
474
+ if (!isActive() || !adjustRounding || !hotkey || attempt >= 8 || candidate - EVM_WEI_PER_RAO < minAmountOutWei)
475
+ throw error;
476
+ const index = candidatePulls.findIndex(
477
+ (pull) => pull.hotkey === hotkey
478
+ );
479
+ if (index < 0 || candidatePulls[index].amountRao <= 1n) throw error;
480
+ const nextPulls = candidatePulls.map(
481
+ (pull, i) => i === index ? { ...pull, amountRao: pull.amountRao - 1n } : pull
482
+ );
483
+ if (!fits(nextPulls)) throw error;
484
+ candidatePulls = nextPulls;
485
+ candidate -= EVM_WEI_PER_RAO;
486
+ }
487
+ }
488
+ }
489
+
490
+ // src/core/addresses.ts
491
+ import {
492
+ AccountId,
493
+ Blake2256,
494
+ getSs58AddressInfo
495
+ } from "@polkadot-api/substrate-bindings";
496
+ import {
497
+ bytesToHex,
498
+ concat,
499
+ getAddress as getAddress2,
500
+ hexToBytes,
501
+ isAddress,
502
+ stringToBytes,
503
+ zeroAddress
504
+ } from "viem";
505
+ var BITTENSOR_SS58_PREFIX = 42;
506
+ function normalizeEvmAddress(value) {
507
+ const candidate = typeof value === "string" && /^0x[0-9A-F]{40}$/.test(value) ? value.toLowerCase() : value;
508
+ if (!isAddress(candidate) || candidate === zeroAddress) {
509
+ throw new ForeverMoneyError(
510
+ "INVALID_ADDRESS",
511
+ "Expected a non-zero EVM address."
512
+ );
513
+ }
514
+ return getAddress2(candidate);
515
+ }
516
+ function evmToMirrorSS58(evmAddress) {
517
+ return AccountId(BITTENSOR_SS58_PREFIX).dec(
518
+ Blake2256(
519
+ concat([
520
+ stringToBytes("evm:"),
521
+ hexToBytes(normalizeEvmAddress(evmAddress))
522
+ ])
523
+ )
524
+ );
525
+ }
526
+ function decodeBittensorAddress(value) {
527
+ try {
528
+ if (typeof value !== "string" || !value || value.startsWith("0x"))
529
+ throw new Error("Raw hex is not SS58.");
530
+ const info = getSs58AddressInfo(value);
531
+ if (!info.isValid || info.ss58Format !== BITTENSOR_SS58_PREFIX || info.publicKey.length !== 32)
532
+ throw new Error("Invalid Bittensor address.");
533
+ const publicKey = info.publicKey;
534
+ return publicKey;
535
+ } catch {
536
+ throw new ForeverMoneyError(
537
+ "INVALID_SS58",
538
+ "Expected a Bittensor SS58 address with prefix 42."
539
+ );
540
+ }
541
+ }
542
+ function normalizeSS58(value) {
543
+ return AccountId(BITTENSOR_SS58_PREFIX).dec(decodeBittensorAddress(value));
544
+ }
545
+ function isBittensorSS58(value) {
546
+ try {
547
+ decodeBittensorAddress(value);
548
+ return true;
549
+ } catch {
550
+ return false;
551
+ }
552
+ }
553
+ function ss58ToPublicKey(value) {
554
+ return bytesToHex(decodeBittensorAddress(value));
555
+ }
556
+
343
557
  // src/abis/index.ts
344
558
  var ERC20_ABI = Object.freeze([
345
559
  "function allowance(address owner,address spender) view returns (uint256)",
@@ -455,89 +669,6 @@ function createTransactionPlan(plan) {
455
669
  });
456
670
  }
457
671
 
458
- // src/core/validation.ts
459
- import { maxUint256 as maxUint2562, zeroHash, isHex } from "viem";
460
- function assertBigInt(amount, label) {
461
- if (typeof amount !== "bigint") {
462
- throw new ForeverMoneyError(
463
- "INVALID_TRANSACTION_PLAN",
464
- `${label} must be a bigint.`
465
- );
466
- }
467
- }
468
- function assertPositiveAmount(amount, label) {
469
- assertBigInt(amount, label);
470
- if (amount === 0n) {
471
- throw new ForeverMoneyError(
472
- "AMOUNT_ZERO",
473
- `${label} must be greater than zero.`
474
- );
475
- }
476
- if (amount < 0n) {
477
- throw new ForeverMoneyError(
478
- "INVALID_TRANSACTION_PLAN",
479
- `${label} cannot be negative.`
480
- );
481
- }
482
- if (amount > maxUint2562) {
483
- throw new ForeverMoneyError(
484
- "INVALID_TRANSACTION_PLAN",
485
- `${label} exceeds uint256.`
486
- );
487
- }
488
- }
489
- function assertNonNegativeAmount(amount, label) {
490
- assertBigInt(amount, label);
491
- if (amount < 0n) {
492
- throw new ForeverMoneyError(
493
- "INVALID_TRANSACTION_PLAN",
494
- `${label} cannot be negative.`
495
- );
496
- }
497
- if (amount > maxUint2562) {
498
- throw new ForeverMoneyError(
499
- "INVALID_TRANSACTION_PLAN",
500
- `${label} exceeds uint256.`
501
- );
502
- }
503
- }
504
- function assertBoolean(value, label) {
505
- if (typeof value !== "boolean") {
506
- throw new ForeverMoneyError(
507
- "INVALID_TRANSACTION_PLAN",
508
- `${label} must be a boolean.`
509
- );
510
- }
511
- }
512
- function assertArray(value, label) {
513
- if (!Array.isArray(value)) {
514
- throw new ForeverMoneyError(
515
- "INVALID_TRANSACTION_PLAN",
516
- `${label} must be an array.`
517
- );
518
- }
519
- }
520
- function assertRecord(value, label) {
521
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
522
- throw new ForeverMoneyError(
523
- "INVALID_TRANSACTION_PLAN",
524
- `${label} must be an object.`
525
- );
526
- }
527
- }
528
- function normalizeBytes32(value, label) {
529
- if (!isHex(value, { strict: true }) || value.length !== 66) {
530
- throw new ForeverMoneyError(
531
- "INVALID_BYTES32",
532
- `${label} must be a 32-byte hex value.`
533
- );
534
- }
535
- return value.toLowerCase();
536
- }
537
- function normalizeOptionalBytes32(value, label) {
538
- return value === void 0 ? zeroHash : normalizeBytes32(value, label);
539
- }
540
-
541
672
  // src/bridge/plans.ts
542
673
  var erc20Abi = parseAbi(ERC20_ABI);
543
674
  var spokeAbi = parseAbi(SPOKE_GATEWAY_ABI);
@@ -1097,7 +1228,7 @@ async function prepareEvmToSubtensor(provider, input) {
1097
1228
  function prepareBaseToSubtensor(provider, input) {
1098
1229
  return prepareEvmToSubtensor(provider, { ...input, evmChain: "base" });
1099
1230
  }
1100
- async function prepareSubtensorToEvm(provider, input) {
1231
+ async function prepareSubtensorToEvmExact(provider, input) {
1101
1232
  const sender = normalizeEvmAddress(input.sender);
1102
1233
  const recipient = normalizeEvmAddress(input.recipient);
1103
1234
  const asset = bridgeAsset(input.evmChain, input.asset);
@@ -1179,6 +1310,53 @@ async function prepareSubtensorToEvm(provider, input) {
1179
1310
  plan
1180
1311
  });
1181
1312
  }
1313
+ async function prepareSubtensorToEvm(provider, input) {
1314
+ const options = input.stakeRounding;
1315
+ if (options != null && options !== false && typeof options.enabled !== "boolean") {
1316
+ throw new ForeverMoneyError(
1317
+ "INVALID_TRANSACTION_PLAN",
1318
+ "stakeRounding.enabled must be a boolean."
1319
+ );
1320
+ }
1321
+ if (!options || !options.enabled)
1322
+ return prepareSubtensorToEvmExact(provider, input);
1323
+ if (input.source !== "staked" || !input.stakePulls?.length) {
1324
+ throw new ForeverMoneyError(
1325
+ "INVALID_TRANSACTION_PLAN",
1326
+ "Stake rounding requires a staked source with named stakePulls."
1327
+ );
1328
+ }
1329
+ const minimum = minimumSubtensorOutput(input);
1330
+ const result = await estimateRoundedStake({
1331
+ amountWei: input.amountWei,
1332
+ minAmountOutWei: minimum,
1333
+ pulls: input.stakePulls,
1334
+ positions: options.positions,
1335
+ minStakeRao: options.minStakeRao ?? 0n,
1336
+ partnerFeeBps: input.partnerFee?.bps ?? 0,
1337
+ quoteAndEstimate: async (amountWei, stakePulls, minAmountOutWei) => ({
1338
+ prepared: await prepareSubtensorToEvmExact(provider, {
1339
+ ...input,
1340
+ amountWei,
1341
+ stakePulls,
1342
+ minAmountOutWei,
1343
+ stakeRounding: false
1344
+ })
1345
+ })
1346
+ });
1347
+ return Object.freeze({
1348
+ ...result.prepared,
1349
+ stakeRounding: Object.freeze({
1350
+ requestedAmountWei: input.amountWei,
1351
+ amountWei: result.amountWei,
1352
+ minAmountOutWei: result.minAmountOutWei,
1353
+ pulls: result.pulls,
1354
+ simulationComplete: !result.prepared.plan.steps.some(
1355
+ (step2) => step2.kind === "approval"
1356
+ )
1357
+ })
1358
+ });
1359
+ }
1182
1360
  function prepareSubtensorToBase(provider, input) {
1183
1361
  return prepareSubtensorToEvm(provider, { ...input, evmChain: "base" });
1184
1362
  }
@@ -2269,6 +2447,7 @@ export {
2269
2447
  buildWithdrawVaultPlan,
2270
2448
  createForeverMoneyClient,
2271
2449
  destinationChainId,
2450
+ estimateRoundedStake,
2272
2451
  evmChainFromBridgeDirection,
2273
2452
  evmToMirrorSS58,
2274
2453
  feeWithBuffer,
@@ -2290,6 +2469,8 @@ export {
2290
2469
  partnerFeeTaoTopUp,
2291
2470
  sourceChainId,
2292
2471
  ss58ToPublicKey,
2472
+ stakedMinimumOutput,
2473
+ strandedStakeSource,
2293
2474
  toEip1193Transaction,
2294
2475
  toEthersTransaction,
2295
2476
  toViemTransaction,