@forevermoney/sdk 0.5.0 → 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);
@@ -651,6 +782,17 @@ function stakePullsFor(input, amountRao) {
651
782
  }
652
783
  return resolveStakePulls(input.stakePulls, amountRao);
653
784
  }
785
+ function minimumSubtensorOutput(input) {
786
+ const minimum = input.minAmountOutWei ?? input.amountWei;
787
+ assertNonNegativeAmount(minimum, "Minimum output");
788
+ if (minimum === 0n || minimum > input.amountWei) {
789
+ throw new ForeverMoneyError(
790
+ "INVALID_TRANSACTION_PLAN",
791
+ "Minimum output must be positive and no greater than the input amount."
792
+ );
793
+ }
794
+ return minimum;
795
+ }
654
796
  function assertDelivery(value) {
655
797
  if (value !== "liquid" && value !== "staked") {
656
798
  throw new ForeverMoneyError(
@@ -929,6 +1071,7 @@ function buildSubtensorToEvmPlan(input) {
929
1071
  assertAssetMode(input.asset, input.source);
930
1072
  const netuid = sourceNetuid(input);
931
1073
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1074
+ const minAmountOutWei = minimumSubtensorOutput(input);
932
1075
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
933
1076
  const { subtensor } = foreverMoneyDeployment;
934
1077
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -993,7 +1136,7 @@ function buildSubtensorToEvmPlan(input) {
993
1136
  recipient,
994
1137
  taoAmount,
995
1138
  stakedAlphaRao,
996
- input.amountWei,
1139
+ minAmountOutWei,
997
1140
  partnerFee,
998
1141
  pulls
999
1142
  ),
@@ -1085,7 +1228,7 @@ async function prepareEvmToSubtensor(provider, input) {
1085
1228
  function prepareBaseToSubtensor(provider, input) {
1086
1229
  return prepareEvmToSubtensor(provider, { ...input, evmChain: "base" });
1087
1230
  }
1088
- async function prepareSubtensorToEvm(provider, input) {
1231
+ async function prepareSubtensorToEvmExact(provider, input) {
1089
1232
  const sender = normalizeEvmAddress(input.sender);
1090
1233
  const recipient = normalizeEvmAddress(input.recipient);
1091
1234
  const asset = bridgeAsset(input.evmChain, input.asset);
@@ -1093,6 +1236,7 @@ async function prepareSubtensorToEvm(provider, input) {
1093
1236
  assertAssetMode(input.asset, input.source);
1094
1237
  const netuid = sourceNetuid(input);
1095
1238
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1239
+ const minAmountOutWei = minimumSubtensorOutput(input);
1096
1240
  const { subtensor } = foreverMoneyDeployment;
1097
1241
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1098
1242
  const partnerFee = resolvePartnerFee(
@@ -1146,7 +1290,7 @@ async function prepareSubtensorToEvm(provider, input) {
1146
1290
  recipient,
1147
1291
  taoAmount,
1148
1292
  stakedAlphaRao,
1149
- input.amountWei,
1293
+ minAmountOutWei,
1150
1294
  partnerFee,
1151
1295
  stakePullsFor(input, stakedAlphaRao)
1152
1296
  ),
@@ -1166,6 +1310,53 @@ async function prepareSubtensorToEvm(provider, input) {
1166
1310
  plan
1167
1311
  });
1168
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
+ }
1169
1360
  function prepareSubtensorToBase(provider, input) {
1170
1361
  return prepareSubtensorToEvm(provider, { ...input, evmChain: "base" });
1171
1362
  }
@@ -2256,6 +2447,7 @@ export {
2256
2447
  buildWithdrawVaultPlan,
2257
2448
  createForeverMoneyClient,
2258
2449
  destinationChainId,
2450
+ estimateRoundedStake,
2259
2451
  evmChainFromBridgeDirection,
2260
2452
  evmToMirrorSS58,
2261
2453
  feeWithBuffer,
@@ -2277,6 +2469,8 @@ export {
2277
2469
  partnerFeeTaoTopUp,
2278
2470
  sourceChainId,
2279
2471
  ss58ToPublicKey,
2472
+ stakedMinimumOutput,
2473
+ strandedStakeSource,
2280
2474
  toEip1193Transaction,
2281
2475
  toEthersTransaction,
2282
2476
  toViemTransaction,