@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.cjs CHANGED
@@ -52,6 +52,7 @@ __export(index_exports, {
52
52
  buildWithdrawVaultPlan: () => buildWithdrawVaultPlan,
53
53
  createForeverMoneyClient: () => createForeverMoneyClient,
54
54
  destinationChainId: () => destinationChainId,
55
+ estimateRoundedStake: () => estimateRoundedStake,
55
56
  evmChainFromBridgeDirection: () => evmChainFromBridgeDirection,
56
57
  evmToMirrorSS58: () => evmToMirrorSS58,
57
58
  feeWithBuffer: () => feeWithBuffer,
@@ -73,6 +74,8 @@ __export(index_exports, {
73
74
  partnerFeeTaoTopUp: () => partnerFeeTaoTopUp,
74
75
  sourceChainId: () => sourceChainId,
75
76
  ss58ToPublicKey: () => ss58ToPublicKey,
77
+ stakedMinimumOutput: () => stakedMinimumOutput,
78
+ strandedStakeSource: () => strandedStakeSource,
76
79
  toEip1193Transaction: () => toEip1193Transaction,
77
80
  toEthersTransaction: () => toEthersTransaction,
78
81
  toViemTransaction: () => toViemTransaction,
@@ -80,80 +83,8 @@ __export(index_exports, {
80
83
  });
81
84
  module.exports = __toCommonJS(index_exports);
82
85
 
83
- // src/core/addresses.ts
84
- var import_substrate_bindings = require("@polkadot-api/substrate-bindings");
85
- var import_viem = require("viem");
86
-
87
- // src/core/errors.ts
88
- var ForeverMoneyError = class extends Error {
89
- code;
90
- details;
91
- constructor(code, message, details) {
92
- super(message);
93
- this.name = "ForeverMoneyError";
94
- this.code = code;
95
- this.details = details === void 0 ? void 0 : Object.freeze({ ...details });
96
- }
97
- };
98
-
99
- // src/core/addresses.ts
100
- var BITTENSOR_SS58_PREFIX = 42;
101
- function normalizeEvmAddress(value) {
102
- const candidate = typeof value === "string" && /^0x[0-9A-F]{40}$/.test(value) ? value.toLowerCase() : value;
103
- if (!(0, import_viem.isAddress)(candidate) || candidate === import_viem.zeroAddress) {
104
- throw new ForeverMoneyError(
105
- "INVALID_ADDRESS",
106
- "Expected a non-zero EVM address."
107
- );
108
- }
109
- return (0, import_viem.getAddress)(candidate);
110
- }
111
- function evmToMirrorSS58(evmAddress) {
112
- return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(
113
- (0, import_substrate_bindings.Blake2256)(
114
- (0, import_viem.concat)([
115
- (0, import_viem.stringToBytes)("evm:"),
116
- (0, import_viem.hexToBytes)(normalizeEvmAddress(evmAddress))
117
- ])
118
- )
119
- );
120
- }
121
- function decodeBittensorAddress(value) {
122
- try {
123
- if (typeof value !== "string" || !value || value.startsWith("0x"))
124
- throw new Error("Raw hex is not SS58.");
125
- const info = (0, import_substrate_bindings.getSs58AddressInfo)(value);
126
- if (!info.isValid || info.ss58Format !== BITTENSOR_SS58_PREFIX || info.publicKey.length !== 32)
127
- throw new Error("Invalid Bittensor address.");
128
- const publicKey = info.publicKey;
129
- return publicKey;
130
- } catch {
131
- throw new ForeverMoneyError(
132
- "INVALID_SS58",
133
- "Expected a Bittensor SS58 address with prefix 42."
134
- );
135
- }
136
- }
137
- function normalizeSS58(value) {
138
- return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(decodeBittensorAddress(value));
139
- }
140
- function isBittensorSS58(value) {
141
- try {
142
- decodeBittensorAddress(value);
143
- return true;
144
- } catch {
145
- return false;
146
- }
147
- }
148
- function ss58ToPublicKey(value) {
149
- return (0, import_viem.bytesToHex)(decodeBittensorAddress(value));
150
- }
151
-
152
- // src/core/amounts.ts
153
- var import_viem3 = require("viem");
154
-
155
86
  // src/chains/deployment.ts
156
- var import_viem2 = require("viem");
87
+ var import_viem = require("viem");
157
88
  var BASE_CHAIN_ID = 8453;
158
89
  var ROBINHOOD_CHAIN_ID = 4663;
159
90
  var SUBTENSOR_CHAIN_ID = 964;
@@ -172,34 +103,34 @@ var foreverMoneyDeployment = Object.freeze({
172
103
  chainId: BASE_CHAIN_ID,
173
104
  ccipSelector: BASE_CCIP_SELECTOR,
174
105
  contracts: Object.freeze({
175
- gateway: (0, import_viem2.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
106
+ gateway: (0, import_viem.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
176
107
  // Retired gateways, oldest first; kept only to track transfers sent
177
108
  // through them.
178
109
  legacyGateways: [
179
- (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
110
+ (0, import_viem.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
180
111
  ],
181
- wrappedTao: (0, import_viem2.getAddress)(
112
+ wrappedTao: (0, import_viem.getAddress)(
182
113
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
183
114
  ),
184
- wrappedSn80: (0, import_viem2.getAddress)(
115
+ wrappedSn80: (0, import_viem.getAddress)(
185
116
  "0x6f63d869011f95274498023b4abfc00b30c34378"
186
117
  ),
187
- ccipRouter: (0, import_viem2.getAddress)(
118
+ ccipRouter: (0, import_viem.getAddress)(
188
119
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
189
120
  ),
190
- ccipOffRampFromSubtensor: (0, import_viem2.getAddress)(
121
+ ccipOffRampFromSubtensor: (0, import_viem.getAddress)(
191
122
  "0xf09AFe78d3c7d359b334d7cB88995751F7eC5E13"
192
123
  ),
193
- vaultFactory: (0, import_viem2.getAddress)(
124
+ vaultFactory: (0, import_viem.getAddress)(
194
125
  "0x9b7F3c7aa335D8BF9f91741E541820466735F868"
195
126
  ),
196
- vaultManagerImplementation: (0, import_viem2.getAddress)(
127
+ vaultManagerImplementation: (0, import_viem.getAddress)(
197
128
  "0x235Ad8654fe3a1619a9892eBCaD9361F416168da"
198
129
  ),
199
- batchLiquidityManagerQuery: (0, import_viem2.getAddress)(
130
+ batchLiquidityManagerQuery: (0, import_viem.getAddress)(
200
131
  "0xd117176C5E6809b68500995F59589A8c081565A5"
201
132
  ),
202
- weth: (0, import_viem2.getAddress)("0x4200000000000000000000000000000000000006")
133
+ weth: (0, import_viem.getAddress)("0x4200000000000000000000000000000000000006")
203
134
  }),
204
135
  deploymentBlock: 50098800
205
136
  }),
@@ -209,17 +140,17 @@ var foreverMoneyDeployment = Object.freeze({
209
140
  chainId: ROBINHOOD_CHAIN_ID,
210
141
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
211
142
  contracts: Object.freeze({
212
- gateway: (0, import_viem2.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
143
+ gateway: (0, import_viem.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
213
144
  legacyGateways: [
214
- (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
145
+ (0, import_viem.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
215
146
  ],
216
- wrappedTao: (0, import_viem2.getAddress)(
147
+ wrappedTao: (0, import_viem.getAddress)(
217
148
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
218
149
  ),
219
- ccipRouter: (0, import_viem2.getAddress)(
150
+ ccipRouter: (0, import_viem.getAddress)(
220
151
  "0x06fC836cf9839B1cd891C440A0a45242DA6Ae1c9"
221
152
  ),
222
- ccipOffRampFromSubtensor: (0, import_viem2.getAddress)(
153
+ ccipOffRampFromSubtensor: (0, import_viem.getAddress)(
223
154
  "0xcDca5D374e46A6DDDab50bD2D9acB8c796eC35C3"
224
155
  )
225
156
  })
@@ -229,32 +160,32 @@ var foreverMoneyDeployment = Object.freeze({
229
160
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
230
161
  contracts: Object.freeze({
231
162
  // V5.1 AlphaGateway: multi-validator staked input (tao-bridge 485c898).
232
- gateway: (0, import_viem2.getAddress)("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
163
+ gateway: (0, import_viem.getAddress)("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
233
164
  // V4 and V5 hubs. The spokes still deliver to the V5 hub (their hub
234
165
  // pointer is immutable), so it stays here for delivery tracking.
235
166
  legacyGateways: [
236
- (0, import_viem2.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
237
- (0, import_viem2.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
167
+ (0, import_viem.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
168
+ (0, import_viem.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
238
169
  ],
239
- alphaVault: (0, import_viem2.getAddress)(
170
+ alphaVault: (0, import_viem.getAddress)(
240
171
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
241
172
  ),
242
- wrappedTao: (0, import_viem2.getAddress)(
173
+ wrappedTao: (0, import_viem.getAddress)(
243
174
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
244
175
  ),
245
- wrappedSn80: (0, import_viem2.getAddress)(
176
+ wrappedSn80: (0, import_viem.getAddress)(
246
177
  "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
247
178
  ),
248
- ccipRouter: (0, import_viem2.getAddress)(
179
+ ccipRouter: (0, import_viem.getAddress)(
249
180
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
250
181
  ),
251
- ccipOffRampFromBase: (0, import_viem2.getAddress)(
182
+ ccipOffRampFromBase: (0, import_viem.getAddress)(
252
183
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
253
184
  ),
254
- ccipOffRampFromRobinhood: (0, import_viem2.getAddress)(
185
+ ccipOffRampFromRobinhood: (0, import_viem.getAddress)(
255
186
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
256
187
  ),
257
- stakingPrecompile: (0, import_viem2.getAddress)(
188
+ stakingPrecompile: (0, import_viem.getAddress)(
258
189
  "0x0000000000000000000000000000000000000805"
259
190
  )
260
191
  })
@@ -264,6 +195,21 @@ function getForeverMoneyEvmDeployment(chain = "base") {
264
195
  return foreverMoneyDeployment[chain];
265
196
  }
266
197
 
198
+ // src/core/amounts.ts
199
+ var import_viem2 = require("viem");
200
+
201
+ // src/core/errors.ts
202
+ var ForeverMoneyError = class extends Error {
203
+ code;
204
+ details;
205
+ constructor(code, message, details) {
206
+ super(message);
207
+ this.name = "ForeverMoneyError";
208
+ this.code = code;
209
+ this.details = details === void 0 ? void 0 : Object.freeze({ ...details });
210
+ }
211
+ };
212
+
267
213
  // src/core/amounts.ts
268
214
  var BASIS_POINTS = 10000n;
269
215
  var NETWORK_FEE_BUFFER_BPS = 200n;
@@ -284,7 +230,7 @@ function parseTaoAmount(value) {
284
230
  }
285
231
  let amount;
286
232
  try {
287
- amount = (0, import_viem3.parseUnits)(value, 18);
233
+ amount = (0, import_viem2.parseUnits)(value, 18);
288
234
  } catch {
289
235
  throw new ForeverMoneyError(
290
236
  "AMOUNT_NOT_WHOLE_RAO",
@@ -313,7 +259,7 @@ function assertWholeRao(amountWei) {
313
259
  "The amount cannot be negative."
314
260
  );
315
261
  }
316
- if (amountWei > import_viem3.maxUint256) {
262
+ if (amountWei > import_viem2.maxUint256) {
317
263
  throw new ForeverMoneyError(
318
264
  "INVALID_TRANSACTION_PLAN",
319
265
  "The amount exceeds uint256."
@@ -339,7 +285,7 @@ function formatTaoAmount(amountWei) {
339
285
  "A TAO amount cannot be negative."
340
286
  );
341
287
  }
342
- if (amountWei > import_viem3.maxUint256) {
288
+ if (amountWei > import_viem2.maxUint256) {
343
289
  throw new ForeverMoneyError(
344
290
  "INVALID_TRANSACTION_PLAN",
345
291
  "The TAO amount exceeds uint256."
@@ -351,7 +297,7 @@ function formatTaoAmount(amountWei) {
351
297
  "TAO amounts must resolve to a whole RAO."
352
298
  );
353
299
  }
354
- const formatted = (0, import_viem3.formatUnits)(amountWei, 18);
300
+ const formatted = (0, import_viem2.formatUnits)(amountWei, 18);
355
301
  return formatted.includes(".") ? formatted : `${formatted}.0`;
356
302
  }
357
303
  function mulDivCeil(value, numerator, denominator) {
@@ -375,7 +321,7 @@ function feeWithBuffer(fee) {
375
321
  BASIS_POINTS + NETWORK_FEE_BUFFER_BPS,
376
322
  BASIS_POINTS
377
323
  );
378
- if (buffered > import_viem3.maxUint256) {
324
+ if (buffered > import_viem2.maxUint256) {
379
325
  throw new ForeverMoneyError(
380
326
  "INVALID_TRANSACTION_PLAN",
381
327
  "The buffered network fee exceeds uint256."
@@ -401,7 +347,7 @@ function gasLimitWithBuffer(gasLimit) {
401
347
  BASIS_POINTS + GAS_LIMIT_BUFFER_BPS,
402
348
  BASIS_POINTS
403
349
  );
404
- if (buffered > import_viem3.maxUint256) {
350
+ if (buffered > import_viem2.maxUint256) {
405
351
  throw new ForeverMoneyError(
406
352
  "INVALID_TRANSACTION_PLAN",
407
353
  "The buffered gas limit exceeds uint256."
@@ -410,6 +356,277 @@ function gasLimitWithBuffer(gasLimit) {
410
356
  return buffered;
411
357
  }
412
358
 
359
+ // src/core/validation.ts
360
+ var import_viem3 = require("viem");
361
+ function assertBigInt(amount, label) {
362
+ if (typeof amount !== "bigint") {
363
+ throw new ForeverMoneyError(
364
+ "INVALID_TRANSACTION_PLAN",
365
+ `${label} must be a bigint.`
366
+ );
367
+ }
368
+ }
369
+ function assertPositiveAmount(amount, label) {
370
+ assertBigInt(amount, label);
371
+ if (amount === 0n) {
372
+ throw new ForeverMoneyError(
373
+ "AMOUNT_ZERO",
374
+ `${label} must be greater than zero.`
375
+ );
376
+ }
377
+ if (amount < 0n) {
378
+ throw new ForeverMoneyError(
379
+ "INVALID_TRANSACTION_PLAN",
380
+ `${label} cannot be negative.`
381
+ );
382
+ }
383
+ if (amount > import_viem3.maxUint256) {
384
+ throw new ForeverMoneyError(
385
+ "INVALID_TRANSACTION_PLAN",
386
+ `${label} exceeds uint256.`
387
+ );
388
+ }
389
+ }
390
+ function assertNonNegativeAmount(amount, label) {
391
+ assertBigInt(amount, label);
392
+ if (amount < 0n) {
393
+ throw new ForeverMoneyError(
394
+ "INVALID_TRANSACTION_PLAN",
395
+ `${label} cannot be negative.`
396
+ );
397
+ }
398
+ if (amount > import_viem3.maxUint256) {
399
+ throw new ForeverMoneyError(
400
+ "INVALID_TRANSACTION_PLAN",
401
+ `${label} exceeds uint256.`
402
+ );
403
+ }
404
+ }
405
+ function assertBoolean(value, label) {
406
+ if (typeof value !== "boolean") {
407
+ throw new ForeverMoneyError(
408
+ "INVALID_TRANSACTION_PLAN",
409
+ `${label} must be a boolean.`
410
+ );
411
+ }
412
+ }
413
+ function assertArray(value, label) {
414
+ if (!Array.isArray(value)) {
415
+ throw new ForeverMoneyError(
416
+ "INVALID_TRANSACTION_PLAN",
417
+ `${label} must be an array.`
418
+ );
419
+ }
420
+ }
421
+ function assertRecord(value, label) {
422
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
423
+ throw new ForeverMoneyError(
424
+ "INVALID_TRANSACTION_PLAN",
425
+ `${label} must be an object.`
426
+ );
427
+ }
428
+ }
429
+ function normalizeBytes32(value, label) {
430
+ if (!(0, import_viem3.isHex)(value, { strict: true }) || value.length !== 66) {
431
+ throw new ForeverMoneyError(
432
+ "INVALID_BYTES32",
433
+ `${label} must be a 32-byte hex value.`
434
+ );
435
+ }
436
+ return value.toLowerCase();
437
+ }
438
+ function normalizeOptionalBytes32(value, label) {
439
+ return value === void 0 ? import_viem3.zeroHash : normalizeBytes32(value, label);
440
+ }
441
+
442
+ // src/bridge/stake-rounding.ts
443
+ function invalid(message) {
444
+ throw new ForeverMoneyError("INVALID_TRANSACTION_PLAN", message);
445
+ }
446
+ function stakedMinimumOutput(amountWei, sourceCount = 1) {
447
+ assertWholeRao(amountWei);
448
+ if (!Number.isInteger(sourceCount) || sourceCount < 1 || sourceCount > 16)
449
+ invalid("Source count must be between 1 and 16.");
450
+ const rao = amountWei / EVM_WEI_PER_RAO;
451
+ const dust = 2n * BigInt(sourceCount) + 2n;
452
+ return (rao > dust ? rao - dust : rao) * EVM_WEI_PER_RAO;
453
+ }
454
+ function strandedStakeSource(error) {
455
+ const queue = [error];
456
+ const seen = /* @__PURE__ */ new Set();
457
+ while (queue.length && seen.size < 20) {
458
+ const entry = queue.shift();
459
+ if (typeof entry === "string" && /^0x5408a598[0-9a-f]{128}$/i.test(entry))
460
+ return `0x${entry.slice(10, 74)}`.toLowerCase();
461
+ if (entry && typeof entry === "object" && !seen.has(entry)) {
462
+ seen.add(entry);
463
+ for (const key of [
464
+ "data",
465
+ "error",
466
+ "info",
467
+ "cause",
468
+ "details",
469
+ "rpcData"
470
+ ]) {
471
+ const nested = entry[key];
472
+ if (nested != null) queue.push(nested);
473
+ }
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ async function estimateRoundedStake({
479
+ amountWei,
480
+ minAmountOutWei,
481
+ pulls,
482
+ positions,
483
+ minStakeRao = 0n,
484
+ adjustRounding = true,
485
+ partnerFeeBps = 0,
486
+ quoteAndEstimate,
487
+ isActive = () => true
488
+ }) {
489
+ assertWholeRao(amountWei);
490
+ assertPositiveAmount(minAmountOutWei, "Minimum output");
491
+ if (minAmountOutWei > amountWei)
492
+ invalid("Minimum output cannot exceed input.");
493
+ assertBoolean(adjustRounding, "adjustRounding");
494
+ assertNonNegativeAmount(minStakeRao, "Minimum remaining stake");
495
+ if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0 || partnerFeeBps > 1e4)
496
+ invalid("Invalid partner fee basis points.");
497
+ if (!Array.isArray(pulls) || pulls.length < 1 || pulls.length > 16)
498
+ invalid("Provide between 1 and 16 stake pulls.");
499
+ const keys = /* @__PURE__ */ new Set();
500
+ const originalPulls = pulls.map((pull) => {
501
+ const hotkey = normalizeBytes32(pull.hotkey, "Validator").toLowerCase();
502
+ if (keys.has(hotkey)) invalid("Duplicate stake source.");
503
+ keys.add(hotkey);
504
+ assertPositiveAmount(pull.amountRao, "Stake pull");
505
+ return { hotkey, amountRao: pull.amountRao };
506
+ });
507
+ if (originalPulls.reduce((sum, pull) => sum + pull.amountRao, 0n) * EVM_WEI_PER_RAO !== amountWei)
508
+ invalid("Stake pulls must sum to the input amount.");
509
+ const balances = /* @__PURE__ */ new Map();
510
+ if (!Array.isArray(positions)) invalid("Stake positions must be an array.");
511
+ for (const position of positions) {
512
+ const key = normalizeBytes32(position.hotkey, "Validator").toLowerCase();
513
+ if (balances.has(key)) invalid("Duplicate stake position.");
514
+ assertNonNegativeAmount(position.stakeRao, "Stake balance");
515
+ balances.set(key, position.stakeRao);
516
+ }
517
+ const fits = (candidatePulls2) => {
518
+ const total = candidatePulls2.reduce(
519
+ (sum, pull) => sum + pull.amountRao,
520
+ 0n
521
+ );
522
+ const cut = total * BigInt(partnerFeeBps) / 10000n;
523
+ const shares = candidatePulls2.map(
524
+ (pull, index) => index ? cut * pull.amountRao / total : 0n
525
+ );
526
+ shares[0] = cut - shares.reduce((sum, share) => sum + share, 0n);
527
+ return candidatePulls2.every((pull, index) => {
528
+ const balance = balances.get(pull.hotkey);
529
+ if (balance == null) return false;
530
+ const remainder = balance - pull.amountRao - shares[index];
531
+ return remainder === 0n || remainder > 0n && remainder >= minStakeRao;
532
+ });
533
+ };
534
+ if (!fits(originalPulls))
535
+ invalid(
536
+ "Stake positions cannot cover the pulls, partner fee, and minimum remainder."
537
+ );
538
+ let candidate = amountWei;
539
+ let candidatePulls = originalPulls;
540
+ for (let attempt = 0; ; attempt++) {
541
+ if (!isActive()) throw new Error("Stake quote cancelled.");
542
+ try {
543
+ const result = await quoteAndEstimate(
544
+ candidate,
545
+ candidatePulls,
546
+ minAmountOutWei
547
+ );
548
+ if (!isActive()) throw new Error("Stake quote cancelled.");
549
+ return Object.freeze({
550
+ ...result,
551
+ amountWei: candidate,
552
+ pulls: Object.freeze(
553
+ candidatePulls.map((pull) => Object.freeze({ ...pull }))
554
+ ),
555
+ minAmountOutWei
556
+ });
557
+ } catch (error) {
558
+ const hotkey = strandedStakeSource(error);
559
+ if (!isActive() || !adjustRounding || !hotkey || attempt >= 8 || candidate - EVM_WEI_PER_RAO < minAmountOutWei)
560
+ throw error;
561
+ const index = candidatePulls.findIndex(
562
+ (pull) => pull.hotkey === hotkey
563
+ );
564
+ if (index < 0 || candidatePulls[index].amountRao <= 1n) throw error;
565
+ const nextPulls = candidatePulls.map(
566
+ (pull, i) => i === index ? { ...pull, amountRao: pull.amountRao - 1n } : pull
567
+ );
568
+ if (!fits(nextPulls)) throw error;
569
+ candidatePulls = nextPulls;
570
+ candidate -= EVM_WEI_PER_RAO;
571
+ }
572
+ }
573
+ }
574
+
575
+ // src/core/addresses.ts
576
+ var import_substrate_bindings = require("@polkadot-api/substrate-bindings");
577
+ var import_viem4 = require("viem");
578
+ var BITTENSOR_SS58_PREFIX = 42;
579
+ function normalizeEvmAddress(value) {
580
+ const candidate = typeof value === "string" && /^0x[0-9A-F]{40}$/.test(value) ? value.toLowerCase() : value;
581
+ if (!(0, import_viem4.isAddress)(candidate) || candidate === import_viem4.zeroAddress) {
582
+ throw new ForeverMoneyError(
583
+ "INVALID_ADDRESS",
584
+ "Expected a non-zero EVM address."
585
+ );
586
+ }
587
+ return (0, import_viem4.getAddress)(candidate);
588
+ }
589
+ function evmToMirrorSS58(evmAddress) {
590
+ return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(
591
+ (0, import_substrate_bindings.Blake2256)(
592
+ (0, import_viem4.concat)([
593
+ (0, import_viem4.stringToBytes)("evm:"),
594
+ (0, import_viem4.hexToBytes)(normalizeEvmAddress(evmAddress))
595
+ ])
596
+ )
597
+ );
598
+ }
599
+ function decodeBittensorAddress(value) {
600
+ try {
601
+ if (typeof value !== "string" || !value || value.startsWith("0x"))
602
+ throw new Error("Raw hex is not SS58.");
603
+ const info = (0, import_substrate_bindings.getSs58AddressInfo)(value);
604
+ if (!info.isValid || info.ss58Format !== BITTENSOR_SS58_PREFIX || info.publicKey.length !== 32)
605
+ throw new Error("Invalid Bittensor address.");
606
+ const publicKey = info.publicKey;
607
+ return publicKey;
608
+ } catch {
609
+ throw new ForeverMoneyError(
610
+ "INVALID_SS58",
611
+ "Expected a Bittensor SS58 address with prefix 42."
612
+ );
613
+ }
614
+ }
615
+ function normalizeSS58(value) {
616
+ return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(decodeBittensorAddress(value));
617
+ }
618
+ function isBittensorSS58(value) {
619
+ try {
620
+ decodeBittensorAddress(value);
621
+ return true;
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
626
+ function ss58ToPublicKey(value) {
627
+ return (0, import_viem4.bytesToHex)(decodeBittensorAddress(value));
628
+ }
629
+
413
630
  // src/abis/index.ts
414
631
  var ERC20_ABI = Object.freeze([
415
632
  "function allowance(address owner,address spender) view returns (uint256)",
@@ -504,7 +721,7 @@ var foreverMoneyAbis = Object.freeze({
504
721
  var import_viem6 = require("viem");
505
722
 
506
723
  // src/core/plans.ts
507
- var import_viem4 = require("viem");
724
+ var import_viem5 = require("viem");
508
725
  function createTransactionPlan(plan) {
509
726
  const steps = plan.steps.map(
510
727
  (step2) => Object.freeze({
@@ -521,93 +738,10 @@ function createTransactionPlan(plan) {
521
738
  };
522
739
  return Object.freeze({
523
740
  ...versionedPlan,
524
- hash: (0, import_viem4.keccak256)((0, import_viem4.stringToBytes)(JSON.stringify(versionedPlan)))
741
+ hash: (0, import_viem5.keccak256)((0, import_viem5.stringToBytes)(JSON.stringify(versionedPlan)))
525
742
  });
526
743
  }
527
744
 
528
- // src/core/validation.ts
529
- var import_viem5 = require("viem");
530
- function assertBigInt(amount, label) {
531
- if (typeof amount !== "bigint") {
532
- throw new ForeverMoneyError(
533
- "INVALID_TRANSACTION_PLAN",
534
- `${label} must be a bigint.`
535
- );
536
- }
537
- }
538
- function assertPositiveAmount(amount, label) {
539
- assertBigInt(amount, label);
540
- if (amount === 0n) {
541
- throw new ForeverMoneyError(
542
- "AMOUNT_ZERO",
543
- `${label} must be greater than zero.`
544
- );
545
- }
546
- if (amount < 0n) {
547
- throw new ForeverMoneyError(
548
- "INVALID_TRANSACTION_PLAN",
549
- `${label} cannot be negative.`
550
- );
551
- }
552
- if (amount > import_viem5.maxUint256) {
553
- throw new ForeverMoneyError(
554
- "INVALID_TRANSACTION_PLAN",
555
- `${label} exceeds uint256.`
556
- );
557
- }
558
- }
559
- function assertNonNegativeAmount(amount, label) {
560
- assertBigInt(amount, label);
561
- if (amount < 0n) {
562
- throw new ForeverMoneyError(
563
- "INVALID_TRANSACTION_PLAN",
564
- `${label} cannot be negative.`
565
- );
566
- }
567
- if (amount > import_viem5.maxUint256) {
568
- throw new ForeverMoneyError(
569
- "INVALID_TRANSACTION_PLAN",
570
- `${label} exceeds uint256.`
571
- );
572
- }
573
- }
574
- function assertBoolean(value, label) {
575
- if (typeof value !== "boolean") {
576
- throw new ForeverMoneyError(
577
- "INVALID_TRANSACTION_PLAN",
578
- `${label} must be a boolean.`
579
- );
580
- }
581
- }
582
- function assertArray(value, label) {
583
- if (!Array.isArray(value)) {
584
- throw new ForeverMoneyError(
585
- "INVALID_TRANSACTION_PLAN",
586
- `${label} must be an array.`
587
- );
588
- }
589
- }
590
- function assertRecord(value, label) {
591
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
592
- throw new ForeverMoneyError(
593
- "INVALID_TRANSACTION_PLAN",
594
- `${label} must be an object.`
595
- );
596
- }
597
- }
598
- function normalizeBytes32(value, label) {
599
- if (!(0, import_viem5.isHex)(value, { strict: true }) || value.length !== 66) {
600
- throw new ForeverMoneyError(
601
- "INVALID_BYTES32",
602
- `${label} must be a 32-byte hex value.`
603
- );
604
- }
605
- return value.toLowerCase();
606
- }
607
- function normalizeOptionalBytes32(value, label) {
608
- return value === void 0 ? import_viem5.zeroHash : normalizeBytes32(value, label);
609
- }
610
-
611
745
  // src/bridge/plans.ts
612
746
  var erc20Abi = (0, import_viem6.parseAbi)(ERC20_ABI);
613
747
  var spokeAbi = (0, import_viem6.parseAbi)(SPOKE_GATEWAY_ABI);
@@ -721,6 +855,17 @@ function stakePullsFor(input, amountRao) {
721
855
  }
722
856
  return resolveStakePulls(input.stakePulls, amountRao);
723
857
  }
858
+ function minimumSubtensorOutput(input) {
859
+ const minimum = input.minAmountOutWei ?? input.amountWei;
860
+ assertNonNegativeAmount(minimum, "Minimum output");
861
+ if (minimum === 0n || minimum > input.amountWei) {
862
+ throw new ForeverMoneyError(
863
+ "INVALID_TRANSACTION_PLAN",
864
+ "Minimum output must be positive and no greater than the input amount."
865
+ );
866
+ }
867
+ return minimum;
868
+ }
724
869
  function assertDelivery(value) {
725
870
  if (value !== "liquid" && value !== "staked") {
726
871
  throw new ForeverMoneyError(
@@ -999,6 +1144,7 @@ function buildSubtensorToEvmPlan(input) {
999
1144
  assertAssetMode(input.asset, input.source);
1000
1145
  const netuid = sourceNetuid(input);
1001
1146
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1147
+ const minAmountOutWei = minimumSubtensorOutput(input);
1002
1148
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
1003
1149
  const { subtensor } = foreverMoneyDeployment;
1004
1150
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -1063,7 +1209,7 @@ function buildSubtensorToEvmPlan(input) {
1063
1209
  recipient,
1064
1210
  taoAmount,
1065
1211
  stakedAlphaRao,
1066
- input.amountWei,
1212
+ minAmountOutWei,
1067
1213
  partnerFee,
1068
1214
  pulls
1069
1215
  ),
@@ -1155,7 +1301,7 @@ async function prepareEvmToSubtensor(provider, input) {
1155
1301
  function prepareBaseToSubtensor(provider, input) {
1156
1302
  return prepareEvmToSubtensor(provider, { ...input, evmChain: "base" });
1157
1303
  }
1158
- async function prepareSubtensorToEvm(provider, input) {
1304
+ async function prepareSubtensorToEvmExact(provider, input) {
1159
1305
  const sender = normalizeEvmAddress(input.sender);
1160
1306
  const recipient = normalizeEvmAddress(input.recipient);
1161
1307
  const asset = bridgeAsset(input.evmChain, input.asset);
@@ -1163,6 +1309,7 @@ async function prepareSubtensorToEvm(provider, input) {
1163
1309
  assertAssetMode(input.asset, input.source);
1164
1310
  const netuid = sourceNetuid(input);
1165
1311
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1312
+ const minAmountOutWei = minimumSubtensorOutput(input);
1166
1313
  const { subtensor } = foreverMoneyDeployment;
1167
1314
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1168
1315
  const partnerFee = resolvePartnerFee(
@@ -1216,7 +1363,7 @@ async function prepareSubtensorToEvm(provider, input) {
1216
1363
  recipient,
1217
1364
  taoAmount,
1218
1365
  stakedAlphaRao,
1219
- input.amountWei,
1366
+ minAmountOutWei,
1220
1367
  partnerFee,
1221
1368
  stakePullsFor(input, stakedAlphaRao)
1222
1369
  ),
@@ -1236,6 +1383,53 @@ async function prepareSubtensorToEvm(provider, input) {
1236
1383
  plan
1237
1384
  });
1238
1385
  }
1386
+ async function prepareSubtensorToEvm(provider, input) {
1387
+ const options = input.stakeRounding;
1388
+ if (options != null && options !== false && typeof options.enabled !== "boolean") {
1389
+ throw new ForeverMoneyError(
1390
+ "INVALID_TRANSACTION_PLAN",
1391
+ "stakeRounding.enabled must be a boolean."
1392
+ );
1393
+ }
1394
+ if (!options || !options.enabled)
1395
+ return prepareSubtensorToEvmExact(provider, input);
1396
+ if (input.source !== "staked" || !input.stakePulls?.length) {
1397
+ throw new ForeverMoneyError(
1398
+ "INVALID_TRANSACTION_PLAN",
1399
+ "Stake rounding requires a staked source with named stakePulls."
1400
+ );
1401
+ }
1402
+ const minimum = minimumSubtensorOutput(input);
1403
+ const result = await estimateRoundedStake({
1404
+ amountWei: input.amountWei,
1405
+ minAmountOutWei: minimum,
1406
+ pulls: input.stakePulls,
1407
+ positions: options.positions,
1408
+ minStakeRao: options.minStakeRao ?? 0n,
1409
+ partnerFeeBps: input.partnerFee?.bps ?? 0,
1410
+ quoteAndEstimate: async (amountWei, stakePulls, minAmountOutWei) => ({
1411
+ prepared: await prepareSubtensorToEvmExact(provider, {
1412
+ ...input,
1413
+ amountWei,
1414
+ stakePulls,
1415
+ minAmountOutWei,
1416
+ stakeRounding: false
1417
+ })
1418
+ })
1419
+ });
1420
+ return Object.freeze({
1421
+ ...result.prepared,
1422
+ stakeRounding: Object.freeze({
1423
+ requestedAmountWei: input.amountWei,
1424
+ amountWei: result.amountWei,
1425
+ minAmountOutWei: result.minAmountOutWei,
1426
+ pulls: result.pulls,
1427
+ simulationComplete: !result.prepared.plan.steps.some(
1428
+ (step2) => step2.kind === "approval"
1429
+ )
1430
+ })
1431
+ });
1432
+ }
1239
1433
  function prepareSubtensorToBase(provider, input) {
1240
1434
  return prepareSubtensorToEvm(provider, { ...input, evmChain: "base" });
1241
1435
  }