@forevermoney/sdk 0.5.1 → 0.5.3

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
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  BASE_CCIP_SELECTOR: () => BASE_CCIP_SELECTOR,
24
24
  BASE_CHAIN_ID: () => BASE_CHAIN_ID,
25
25
  BASIS_POINTS: () => BASIS_POINTS,
26
+ EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT: () => EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT,
26
27
  EVM_WEI_PER_RAO: () => EVM_WEI_PER_RAO,
27
28
  FOREVERMONEY_DEPLOYMENT_VERSION: () => FOREVERMONEY_DEPLOYMENT_VERSION,
28
29
  ForeverMoneyError: () => ForeverMoneyError,
@@ -52,6 +53,7 @@ __export(index_exports, {
52
53
  buildWithdrawVaultPlan: () => buildWithdrawVaultPlan,
53
54
  createForeverMoneyClient: () => createForeverMoneyClient,
54
55
  destinationChainId: () => destinationChainId,
56
+ estimateRoundedStake: () => estimateRoundedStake,
55
57
  evmChainFromBridgeDirection: () => evmChainFromBridgeDirection,
56
58
  evmToMirrorSS58: () => evmToMirrorSS58,
57
59
  feeWithBuffer: () => feeWithBuffer,
@@ -73,6 +75,8 @@ __export(index_exports, {
73
75
  partnerFeeTaoTopUp: () => partnerFeeTaoTopUp,
74
76
  sourceChainId: () => sourceChainId,
75
77
  ss58ToPublicKey: () => ss58ToPublicKey,
78
+ stakedMinimumOutput: () => stakedMinimumOutput,
79
+ strandedStakeSource: () => strandedStakeSource,
76
80
  toEip1193Transaction: () => toEip1193Transaction,
77
81
  toEthersTransaction: () => toEthersTransaction,
78
82
  toViemTransaction: () => toViemTransaction,
@@ -80,80 +84,8 @@ __export(index_exports, {
80
84
  });
81
85
  module.exports = __toCommonJS(index_exports);
82
86
 
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
87
  // src/chains/deployment.ts
156
- var import_viem2 = require("viem");
88
+ var import_viem = require("viem");
157
89
  var BASE_CHAIN_ID = 8453;
158
90
  var ROBINHOOD_CHAIN_ID = 4663;
159
91
  var SUBTENSOR_CHAIN_ID = 964;
@@ -172,34 +104,34 @@ var foreverMoneyDeployment = Object.freeze({
172
104
  chainId: BASE_CHAIN_ID,
173
105
  ccipSelector: BASE_CCIP_SELECTOR,
174
106
  contracts: Object.freeze({
175
- gateway: (0, import_viem2.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
107
+ gateway: (0, import_viem.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
176
108
  // Retired gateways, oldest first; kept only to track transfers sent
177
109
  // through them.
178
110
  legacyGateways: [
179
- (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
111
+ (0, import_viem.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
180
112
  ],
181
- wrappedTao: (0, import_viem2.getAddress)(
113
+ wrappedTao: (0, import_viem.getAddress)(
182
114
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
183
115
  ),
184
- wrappedSn80: (0, import_viem2.getAddress)(
116
+ wrappedSn80: (0, import_viem.getAddress)(
185
117
  "0x6f63d869011f95274498023b4abfc00b30c34378"
186
118
  ),
187
- ccipRouter: (0, import_viem2.getAddress)(
119
+ ccipRouter: (0, import_viem.getAddress)(
188
120
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
189
121
  ),
190
- ccipOffRampFromSubtensor: (0, import_viem2.getAddress)(
122
+ ccipOffRampFromSubtensor: (0, import_viem.getAddress)(
191
123
  "0xf09AFe78d3c7d359b334d7cB88995751F7eC5E13"
192
124
  ),
193
- vaultFactory: (0, import_viem2.getAddress)(
125
+ vaultFactory: (0, import_viem.getAddress)(
194
126
  "0x9b7F3c7aa335D8BF9f91741E541820466735F868"
195
127
  ),
196
- vaultManagerImplementation: (0, import_viem2.getAddress)(
128
+ vaultManagerImplementation: (0, import_viem.getAddress)(
197
129
  "0x235Ad8654fe3a1619a9892eBCaD9361F416168da"
198
130
  ),
199
- batchLiquidityManagerQuery: (0, import_viem2.getAddress)(
131
+ batchLiquidityManagerQuery: (0, import_viem.getAddress)(
200
132
  "0xd117176C5E6809b68500995F59589A8c081565A5"
201
133
  ),
202
- weth: (0, import_viem2.getAddress)("0x4200000000000000000000000000000000000006")
134
+ weth: (0, import_viem.getAddress)("0x4200000000000000000000000000000000000006")
203
135
  }),
204
136
  deploymentBlock: 50098800
205
137
  }),
@@ -209,17 +141,17 @@ var foreverMoneyDeployment = Object.freeze({
209
141
  chainId: ROBINHOOD_CHAIN_ID,
210
142
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
211
143
  contracts: Object.freeze({
212
- gateway: (0, import_viem2.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
144
+ gateway: (0, import_viem.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
213
145
  legacyGateways: [
214
- (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
146
+ (0, import_viem.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
215
147
  ],
216
- wrappedTao: (0, import_viem2.getAddress)(
148
+ wrappedTao: (0, import_viem.getAddress)(
217
149
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
218
150
  ),
219
- ccipRouter: (0, import_viem2.getAddress)(
151
+ ccipRouter: (0, import_viem.getAddress)(
220
152
  "0x06fC836cf9839B1cd891C440A0a45242DA6Ae1c9"
221
153
  ),
222
- ccipOffRampFromSubtensor: (0, import_viem2.getAddress)(
154
+ ccipOffRampFromSubtensor: (0, import_viem.getAddress)(
223
155
  "0xcDca5D374e46A6DDDab50bD2D9acB8c796eC35C3"
224
156
  )
225
157
  })
@@ -229,32 +161,32 @@ var foreverMoneyDeployment = Object.freeze({
229
161
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
230
162
  contracts: Object.freeze({
231
163
  // V5.1 AlphaGateway: multi-validator staked input (tao-bridge 485c898).
232
- gateway: (0, import_viem2.getAddress)("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
164
+ gateway: (0, import_viem.getAddress)("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
233
165
  // V4 and V5 hubs. The spokes still deliver to the V5 hub (their hub
234
166
  // pointer is immutable), so it stays here for delivery tracking.
235
167
  legacyGateways: [
236
- (0, import_viem2.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
237
- (0, import_viem2.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
168
+ (0, import_viem.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
169
+ (0, import_viem.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
238
170
  ],
239
- alphaVault: (0, import_viem2.getAddress)(
171
+ alphaVault: (0, import_viem.getAddress)(
240
172
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
241
173
  ),
242
- wrappedTao: (0, import_viem2.getAddress)(
174
+ wrappedTao: (0, import_viem.getAddress)(
243
175
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
244
176
  ),
245
- wrappedSn80: (0, import_viem2.getAddress)(
177
+ wrappedSn80: (0, import_viem.getAddress)(
246
178
  "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
247
179
  ),
248
- ccipRouter: (0, import_viem2.getAddress)(
180
+ ccipRouter: (0, import_viem.getAddress)(
249
181
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
250
182
  ),
251
- ccipOffRampFromBase: (0, import_viem2.getAddress)(
183
+ ccipOffRampFromBase: (0, import_viem.getAddress)(
252
184
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
253
185
  ),
254
- ccipOffRampFromRobinhood: (0, import_viem2.getAddress)(
186
+ ccipOffRampFromRobinhood: (0, import_viem.getAddress)(
255
187
  "0x51a6150400ed9F0Ae240F5D1b15E3b45Fc4339C7"
256
188
  ),
257
- stakingPrecompile: (0, import_viem2.getAddress)(
189
+ stakingPrecompile: (0, import_viem.getAddress)(
258
190
  "0x0000000000000000000000000000000000000805"
259
191
  )
260
192
  })
@@ -264,6 +196,21 @@ function getForeverMoneyEvmDeployment(chain = "base") {
264
196
  return foreverMoneyDeployment[chain];
265
197
  }
266
198
 
199
+ // src/core/amounts.ts
200
+ var import_viem2 = require("viem");
201
+
202
+ // src/core/errors.ts
203
+ var ForeverMoneyError = class extends Error {
204
+ code;
205
+ details;
206
+ constructor(code, message, details) {
207
+ super(message);
208
+ this.name = "ForeverMoneyError";
209
+ this.code = code;
210
+ this.details = details === void 0 ? void 0 : Object.freeze({ ...details });
211
+ }
212
+ };
213
+
267
214
  // src/core/amounts.ts
268
215
  var BASIS_POINTS = 10000n;
269
216
  var NETWORK_FEE_BUFFER_BPS = 200n;
@@ -284,7 +231,7 @@ function parseTaoAmount(value) {
284
231
  }
285
232
  let amount;
286
233
  try {
287
- amount = (0, import_viem3.parseUnits)(value, 18);
234
+ amount = (0, import_viem2.parseUnits)(value, 18);
288
235
  } catch {
289
236
  throw new ForeverMoneyError(
290
237
  "AMOUNT_NOT_WHOLE_RAO",
@@ -313,7 +260,7 @@ function assertWholeRao(amountWei) {
313
260
  "The amount cannot be negative."
314
261
  );
315
262
  }
316
- if (amountWei > import_viem3.maxUint256) {
263
+ if (amountWei > import_viem2.maxUint256) {
317
264
  throw new ForeverMoneyError(
318
265
  "INVALID_TRANSACTION_PLAN",
319
266
  "The amount exceeds uint256."
@@ -339,7 +286,7 @@ function formatTaoAmount(amountWei) {
339
286
  "A TAO amount cannot be negative."
340
287
  );
341
288
  }
342
- if (amountWei > import_viem3.maxUint256) {
289
+ if (amountWei > import_viem2.maxUint256) {
343
290
  throw new ForeverMoneyError(
344
291
  "INVALID_TRANSACTION_PLAN",
345
292
  "The TAO amount exceeds uint256."
@@ -351,7 +298,7 @@ function formatTaoAmount(amountWei) {
351
298
  "TAO amounts must resolve to a whole RAO."
352
299
  );
353
300
  }
354
- const formatted = (0, import_viem3.formatUnits)(amountWei, 18);
301
+ const formatted = (0, import_viem2.formatUnits)(amountWei, 18);
355
302
  return formatted.includes(".") ? formatted : `${formatted}.0`;
356
303
  }
357
304
  function mulDivCeil(value, numerator, denominator) {
@@ -375,7 +322,7 @@ function feeWithBuffer(fee) {
375
322
  BASIS_POINTS + NETWORK_FEE_BUFFER_BPS,
376
323
  BASIS_POINTS
377
324
  );
378
- if (buffered > import_viem3.maxUint256) {
325
+ if (buffered > import_viem2.maxUint256) {
379
326
  throw new ForeverMoneyError(
380
327
  "INVALID_TRANSACTION_PLAN",
381
328
  "The buffered network fee exceeds uint256."
@@ -401,7 +348,7 @@ function gasLimitWithBuffer(gasLimit) {
401
348
  BASIS_POINTS + GAS_LIMIT_BUFFER_BPS,
402
349
  BASIS_POINTS
403
350
  );
404
- if (buffered > import_viem3.maxUint256) {
351
+ if (buffered > import_viem2.maxUint256) {
405
352
  throw new ForeverMoneyError(
406
353
  "INVALID_TRANSACTION_PLAN",
407
354
  "The buffered gas limit exceeds uint256."
@@ -410,6 +357,277 @@ function gasLimitWithBuffer(gasLimit) {
410
357
  return buffered;
411
358
  }
412
359
 
360
+ // src/core/validation.ts
361
+ var import_viem3 = require("viem");
362
+ function assertBigInt(amount, label) {
363
+ if (typeof amount !== "bigint") {
364
+ throw new ForeverMoneyError(
365
+ "INVALID_TRANSACTION_PLAN",
366
+ `${label} must be a bigint.`
367
+ );
368
+ }
369
+ }
370
+ function assertPositiveAmount(amount, label) {
371
+ assertBigInt(amount, label);
372
+ if (amount === 0n) {
373
+ throw new ForeverMoneyError(
374
+ "AMOUNT_ZERO",
375
+ `${label} must be greater than zero.`
376
+ );
377
+ }
378
+ if (amount < 0n) {
379
+ throw new ForeverMoneyError(
380
+ "INVALID_TRANSACTION_PLAN",
381
+ `${label} cannot be negative.`
382
+ );
383
+ }
384
+ if (amount > import_viem3.maxUint256) {
385
+ throw new ForeverMoneyError(
386
+ "INVALID_TRANSACTION_PLAN",
387
+ `${label} exceeds uint256.`
388
+ );
389
+ }
390
+ }
391
+ function assertNonNegativeAmount(amount, label) {
392
+ assertBigInt(amount, label);
393
+ if (amount < 0n) {
394
+ throw new ForeverMoneyError(
395
+ "INVALID_TRANSACTION_PLAN",
396
+ `${label} cannot be negative.`
397
+ );
398
+ }
399
+ if (amount > import_viem3.maxUint256) {
400
+ throw new ForeverMoneyError(
401
+ "INVALID_TRANSACTION_PLAN",
402
+ `${label} exceeds uint256.`
403
+ );
404
+ }
405
+ }
406
+ function assertBoolean(value, label) {
407
+ if (typeof value !== "boolean") {
408
+ throw new ForeverMoneyError(
409
+ "INVALID_TRANSACTION_PLAN",
410
+ `${label} must be a boolean.`
411
+ );
412
+ }
413
+ }
414
+ function assertArray(value, label) {
415
+ if (!Array.isArray(value)) {
416
+ throw new ForeverMoneyError(
417
+ "INVALID_TRANSACTION_PLAN",
418
+ `${label} must be an array.`
419
+ );
420
+ }
421
+ }
422
+ function assertRecord(value, label) {
423
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
424
+ throw new ForeverMoneyError(
425
+ "INVALID_TRANSACTION_PLAN",
426
+ `${label} must be an object.`
427
+ );
428
+ }
429
+ }
430
+ function normalizeBytes32(value, label) {
431
+ if (!(0, import_viem3.isHex)(value, { strict: true }) || value.length !== 66) {
432
+ throw new ForeverMoneyError(
433
+ "INVALID_BYTES32",
434
+ `${label} must be a 32-byte hex value.`
435
+ );
436
+ }
437
+ return value.toLowerCase();
438
+ }
439
+ function normalizeOptionalBytes32(value, label) {
440
+ return value === void 0 ? import_viem3.zeroHash : normalizeBytes32(value, label);
441
+ }
442
+
443
+ // src/bridge/stake-rounding.ts
444
+ function invalid(message) {
445
+ throw new ForeverMoneyError("INVALID_TRANSACTION_PLAN", message);
446
+ }
447
+ function stakedMinimumOutput(amountWei, sourceCount = 1) {
448
+ assertWholeRao(amountWei);
449
+ if (!Number.isInteger(sourceCount) || sourceCount < 1 || sourceCount > 16)
450
+ invalid("Source count must be between 1 and 16.");
451
+ const rao = amountWei / EVM_WEI_PER_RAO;
452
+ const dust = 2n * BigInt(sourceCount) + 2n;
453
+ return (rao > dust ? rao - dust : rao) * EVM_WEI_PER_RAO;
454
+ }
455
+ function strandedStakeSource(error) {
456
+ const queue = [error];
457
+ const seen = /* @__PURE__ */ new Set();
458
+ while (queue.length && seen.size < 20) {
459
+ const entry = queue.shift();
460
+ if (typeof entry === "string" && /^0x5408a598[0-9a-f]{128}$/i.test(entry))
461
+ return `0x${entry.slice(10, 74)}`.toLowerCase();
462
+ if (entry && typeof entry === "object" && !seen.has(entry)) {
463
+ seen.add(entry);
464
+ for (const key of [
465
+ "data",
466
+ "error",
467
+ "info",
468
+ "cause",
469
+ "details",
470
+ "rpcData"
471
+ ]) {
472
+ const nested = entry[key];
473
+ if (nested != null) queue.push(nested);
474
+ }
475
+ }
476
+ }
477
+ return null;
478
+ }
479
+ async function estimateRoundedStake({
480
+ amountWei,
481
+ minAmountOutWei,
482
+ pulls,
483
+ positions,
484
+ minStakeRao = 0n,
485
+ adjustRounding = true,
486
+ partnerFeeBps = 0,
487
+ quoteAndEstimate,
488
+ isActive = () => true
489
+ }) {
490
+ assertWholeRao(amountWei);
491
+ assertPositiveAmount(minAmountOutWei, "Minimum output");
492
+ if (minAmountOutWei > amountWei)
493
+ invalid("Minimum output cannot exceed input.");
494
+ assertBoolean(adjustRounding, "adjustRounding");
495
+ assertNonNegativeAmount(minStakeRao, "Minimum remaining stake");
496
+ if (!Number.isInteger(partnerFeeBps) || partnerFeeBps < 0 || partnerFeeBps > 1e4)
497
+ invalid("Invalid partner fee basis points.");
498
+ if (!Array.isArray(pulls) || pulls.length < 1 || pulls.length > 16)
499
+ invalid("Provide between 1 and 16 stake pulls.");
500
+ const keys = /* @__PURE__ */ new Set();
501
+ const originalPulls = pulls.map((pull) => {
502
+ const hotkey = normalizeBytes32(pull.hotkey, "Validator").toLowerCase();
503
+ if (keys.has(hotkey)) invalid("Duplicate stake source.");
504
+ keys.add(hotkey);
505
+ assertPositiveAmount(pull.amountRao, "Stake pull");
506
+ return { hotkey, amountRao: pull.amountRao };
507
+ });
508
+ if (originalPulls.reduce((sum, pull) => sum + pull.amountRao, 0n) * EVM_WEI_PER_RAO !== amountWei)
509
+ invalid("Stake pulls must sum to the input amount.");
510
+ const balances = /* @__PURE__ */ new Map();
511
+ if (!Array.isArray(positions)) invalid("Stake positions must be an array.");
512
+ for (const position of positions) {
513
+ const key = normalizeBytes32(position.hotkey, "Validator").toLowerCase();
514
+ if (balances.has(key)) invalid("Duplicate stake position.");
515
+ assertNonNegativeAmount(position.stakeRao, "Stake balance");
516
+ balances.set(key, position.stakeRao);
517
+ }
518
+ const fits = (candidatePulls2) => {
519
+ const total = candidatePulls2.reduce(
520
+ (sum, pull) => sum + pull.amountRao,
521
+ 0n
522
+ );
523
+ const cut = total * BigInt(partnerFeeBps) / 10000n;
524
+ const shares = candidatePulls2.map(
525
+ (pull, index) => index ? cut * pull.amountRao / total : 0n
526
+ );
527
+ shares[0] = cut - shares.reduce((sum, share) => sum + share, 0n);
528
+ return candidatePulls2.every((pull, index) => {
529
+ const balance = balances.get(pull.hotkey);
530
+ if (balance == null) return false;
531
+ const remainder = balance - pull.amountRao - shares[index];
532
+ return remainder === 0n || remainder > 0n && remainder >= minStakeRao;
533
+ });
534
+ };
535
+ if (!fits(originalPulls))
536
+ invalid(
537
+ "Stake positions cannot cover the pulls, partner fee, and minimum remainder."
538
+ );
539
+ let candidate = amountWei;
540
+ let candidatePulls = originalPulls;
541
+ for (let attempt = 0; ; attempt++) {
542
+ if (!isActive()) throw new Error("Stake quote cancelled.");
543
+ try {
544
+ const result = await quoteAndEstimate(
545
+ candidate,
546
+ candidatePulls,
547
+ minAmountOutWei
548
+ );
549
+ if (!isActive()) throw new Error("Stake quote cancelled.");
550
+ return Object.freeze({
551
+ ...result,
552
+ amountWei: candidate,
553
+ pulls: Object.freeze(
554
+ candidatePulls.map((pull) => Object.freeze({ ...pull }))
555
+ ),
556
+ minAmountOutWei
557
+ });
558
+ } catch (error) {
559
+ const hotkey = strandedStakeSource(error);
560
+ if (!isActive() || !adjustRounding || !hotkey || attempt >= 8 || candidate - EVM_WEI_PER_RAO < minAmountOutWei)
561
+ throw error;
562
+ const index = candidatePulls.findIndex(
563
+ (pull) => pull.hotkey === hotkey
564
+ );
565
+ if (index < 0 || candidatePulls[index].amountRao <= 1n) throw error;
566
+ const nextPulls = candidatePulls.map(
567
+ (pull, i) => i === index ? { ...pull, amountRao: pull.amountRao - 1n } : pull
568
+ );
569
+ if (!fits(nextPulls)) throw error;
570
+ candidatePulls = nextPulls;
571
+ candidate -= EVM_WEI_PER_RAO;
572
+ }
573
+ }
574
+ }
575
+
576
+ // src/core/addresses.ts
577
+ var import_substrate_bindings = require("@polkadot-api/substrate-bindings");
578
+ var import_viem4 = require("viem");
579
+ var BITTENSOR_SS58_PREFIX = 42;
580
+ function normalizeEvmAddress(value) {
581
+ const candidate = typeof value === "string" && /^0x[0-9A-F]{40}$/.test(value) ? value.toLowerCase() : value;
582
+ if (!(0, import_viem4.isAddress)(candidate) || candidate === import_viem4.zeroAddress) {
583
+ throw new ForeverMoneyError(
584
+ "INVALID_ADDRESS",
585
+ "Expected a non-zero EVM address."
586
+ );
587
+ }
588
+ return (0, import_viem4.getAddress)(candidate);
589
+ }
590
+ function evmToMirrorSS58(evmAddress) {
591
+ return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(
592
+ (0, import_substrate_bindings.Blake2256)(
593
+ (0, import_viem4.concat)([
594
+ (0, import_viem4.stringToBytes)("evm:"),
595
+ (0, import_viem4.hexToBytes)(normalizeEvmAddress(evmAddress))
596
+ ])
597
+ )
598
+ );
599
+ }
600
+ function decodeBittensorAddress(value) {
601
+ try {
602
+ if (typeof value !== "string" || !value || value.startsWith("0x"))
603
+ throw new Error("Raw hex is not SS58.");
604
+ const info = (0, import_substrate_bindings.getSs58AddressInfo)(value);
605
+ if (!info.isValid || info.ss58Format !== BITTENSOR_SS58_PREFIX || info.publicKey.length !== 32)
606
+ throw new Error("Invalid Bittensor address.");
607
+ const publicKey = info.publicKey;
608
+ return publicKey;
609
+ } catch {
610
+ throw new ForeverMoneyError(
611
+ "INVALID_SS58",
612
+ "Expected a Bittensor SS58 address with prefix 42."
613
+ );
614
+ }
615
+ }
616
+ function normalizeSS58(value) {
617
+ return (0, import_substrate_bindings.AccountId)(BITTENSOR_SS58_PREFIX).dec(decodeBittensorAddress(value));
618
+ }
619
+ function isBittensorSS58(value) {
620
+ try {
621
+ decodeBittensorAddress(value);
622
+ return true;
623
+ } catch {
624
+ return false;
625
+ }
626
+ }
627
+ function ss58ToPublicKey(value) {
628
+ return (0, import_viem4.bytesToHex)(decodeBittensorAddress(value));
629
+ }
630
+
413
631
  // src/abis/index.ts
414
632
  var ERC20_ABI = Object.freeze([
415
633
  "function allowance(address owner,address spender) view returns (uint256)",
@@ -428,7 +646,9 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
428
646
  "function maxIntegratorFeeBps() view returns (uint16)",
429
647
  "function bridgeFeeBps() view returns (uint16)",
430
648
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
649
+ "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit) view returns (uint256 fee)",
431
650
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
651
+ "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit) payable returns (bytes32 messageId)",
432
652
  "function quoteBridgeToFinneyWithFee(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit,(address recipient,uint16 bps) integrator) view returns (uint256 fee,uint256 cut,uint256 amountCrossing)",
433
653
  "function bridgeToFinneyWithFee(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
434
654
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
@@ -446,7 +666,6 @@ var ALPHA_GATEWAY_ABI = Object.freeze([
446
666
  "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
447
667
  "function bridgeOutFromValidators(uint64 destSelector,address token,address recipient,uint256 taoAmount,(bytes32 validator,uint256 alphaRao)[] sources,uint256 minTokenOut) payable returns (bytes32 messageId)",
448
668
  "function bridgeOutFromValidatorsWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,(bytes32 validator,uint256 alphaRao)[] sources,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
449
- "function minStakeRequired() view returns (uint256)",
450
669
  "function MAX_STAKE_SOURCES() view returns (uint256)",
451
670
  "function GATEWAY_COLDKEY() view returns (bytes32)",
452
671
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
@@ -504,7 +723,7 @@ var foreverMoneyAbis = Object.freeze({
504
723
  var import_viem6 = require("viem");
505
724
 
506
725
  // src/core/plans.ts
507
- var import_viem4 = require("viem");
726
+ var import_viem5 = require("viem");
508
727
  function createTransactionPlan(plan) {
509
728
  const steps = plan.steps.map(
510
729
  (step2) => Object.freeze({
@@ -521,93 +740,10 @@ function createTransactionPlan(plan) {
521
740
  };
522
741
  return Object.freeze({
523
742
  ...versionedPlan,
524
- hash: (0, import_viem4.keccak256)((0, import_viem4.stringToBytes)(JSON.stringify(versionedPlan)))
743
+ hash: (0, import_viem5.keccak256)((0, import_viem5.stringToBytes)(JSON.stringify(versionedPlan)))
525
744
  });
526
745
  }
527
746
 
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
747
  // src/bridge/plans.ts
612
748
  var erc20Abi = (0, import_viem6.parseAbi)(ERC20_ABI);
613
749
  var spokeAbi = (0, import_viem6.parseAbi)(SPOKE_GATEWAY_ABI);
@@ -616,6 +752,18 @@ var stakingAbi = (0, import_viem6.parseAbi)(STAKING_ABI);
616
752
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
617
753
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
618
754
  var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
755
+ var EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT = 3500000n;
756
+ function resolveDestinationGasLimit(value) {
757
+ const gasLimit = value ?? EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT;
758
+ if (typeof gasLimit !== "bigint" || gasLimit <= 0n) {
759
+ throw new ForeverMoneyError(
760
+ "INVALID_TRANSACTION_PLAN",
761
+ "Destination gas limit must be a positive bigint.",
762
+ { destinationGasLimit: String(gasLimit) }
763
+ );
764
+ }
765
+ return gasLimit;
766
+ }
619
767
  var MAX_PARTNER_FEE_BPS = 1e4;
620
768
  var MAX_STAKE_PULLS = 16;
621
769
  var NO_PARTNER_FEE = {
@@ -792,15 +940,15 @@ function partnerFeeSummary(charged, unit, fee) {
792
940
  if (fee.bps === 0) return "";
793
941
  return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
794
942
  }
795
- function encodeSpokeBridge(token, amountWei, exit, fee) {
943
+ function encodeSpokeBridge(token, amountWei, exit, destinationGasLimit, fee) {
796
944
  return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
797
945
  abi: spokeAbi,
798
946
  functionName: "bridgeToFinney",
799
- args: [token, amountWei, exit]
947
+ args: [token, amountWei, exit, destinationGasLimit]
800
948
  }) : (0, import_viem6.encodeFunctionData)({
801
949
  abi: spokeAbi,
802
950
  functionName: "bridgeToFinneyWithFee",
803
- args: [token, amountWei, exit, 0n, fee]
951
+ args: [token, amountWei, exit, destinationGasLimit, fee]
804
952
  });
805
953
  }
806
954
  function resolveStakePulls(pulls, amountRao) {
@@ -887,7 +1035,7 @@ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaR
887
1035
  args: [...args, fee]
888
1036
  });
889
1037
  }
890
- async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
1038
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, destinationGasLimit, fee) {
891
1039
  const maxBps = await provider.readContract({
892
1040
  address: gateway,
893
1041
  abi: spokeAbi,
@@ -898,7 +1046,7 @@ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee)
898
1046
  address: gateway,
899
1047
  abi: spokeAbi,
900
1048
  functionName: "quoteBridgeToFinneyWithFee",
901
- args: [token, amountWei, exit, 0n, fee]
1049
+ args: [token, amountWei, exit, destinationGasLimit, fee]
902
1050
  });
903
1051
  return networkFee;
904
1052
  }
@@ -944,6 +1092,9 @@ function buildEvmToSubtensorPlan(input) {
944
1092
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
945
1093
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
946
1094
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
1095
+ const destinationGasLimit = resolveDestinationGasLimit(
1096
+ input.destinationGasLimit
1097
+ );
947
1098
  const destination = normalizeSS58(input.destination);
948
1099
  const exit = {
949
1100
  ss58: ss58ToPublicKey(destination),
@@ -987,6 +1138,7 @@ function buildEvmToSubtensorPlan(input) {
987
1138
  asset.evmToken,
988
1139
  input.amountWei,
989
1140
  exit,
1141
+ destinationGasLimit,
990
1142
  partnerFee
991
1143
  ),
992
1144
  value,
@@ -1102,6 +1254,9 @@ async function prepareEvmToSubtensor(provider, input) {
1102
1254
  assertDelivery(input.delivery);
1103
1255
  assertAssetMode(input.asset, input.delivery);
1104
1256
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
1257
+ const destinationGasLimit = resolveDestinationGasLimit(
1258
+ input.destinationGasLimit
1259
+ );
1105
1260
  const destination = normalizeSS58(input.destination);
1106
1261
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1107
1262
  const exit = {
@@ -1126,13 +1281,19 @@ async function prepareEvmToSubtensor(provider, input) {
1126
1281
  address: evm.contracts.gateway,
1127
1282
  abi: spokeAbi,
1128
1283
  functionName: "quoteBridgeToFinney",
1129
- args: [asset.evmToken, input.amountWei, exit]
1284
+ args: [
1285
+ asset.evmToken,
1286
+ input.amountWei,
1287
+ exit,
1288
+ destinationGasLimit
1289
+ ]
1130
1290
  }) : quoteSpokeWithFee(
1131
1291
  provider,
1132
1292
  evm.contracts.gateway,
1133
1293
  asset.evmToken,
1134
1294
  input.amountWei,
1135
1295
  exit,
1296
+ destinationGasLimit,
1136
1297
  partnerFee
1137
1298
  )
1138
1299
  ]);
@@ -1142,6 +1303,7 @@ async function prepareEvmToSubtensor(provider, input) {
1142
1303
  asset.evmToken,
1143
1304
  input.amountWei,
1144
1305
  exit,
1306
+ destinationGasLimit,
1145
1307
  partnerFee
1146
1308
  );
1147
1309
  estimatedBridgeGas = await provider.estimateGas({
@@ -1167,7 +1329,7 @@ async function prepareEvmToSubtensor(provider, input) {
1167
1329
  function prepareBaseToSubtensor(provider, input) {
1168
1330
  return prepareEvmToSubtensor(provider, { ...input, evmChain: "base" });
1169
1331
  }
1170
- async function prepareSubtensorToEvm(provider, input) {
1332
+ async function prepareSubtensorToEvmExact(provider, input) {
1171
1333
  const sender = normalizeEvmAddress(input.sender);
1172
1334
  const recipient = normalizeEvmAddress(input.recipient);
1173
1335
  const asset = bridgeAsset(input.evmChain, input.asset);
@@ -1249,6 +1411,53 @@ async function prepareSubtensorToEvm(provider, input) {
1249
1411
  plan
1250
1412
  });
1251
1413
  }
1414
+ async function prepareSubtensorToEvm(provider, input) {
1415
+ const options = input.stakeRounding;
1416
+ if (options != null && options !== false && typeof options.enabled !== "boolean") {
1417
+ throw new ForeverMoneyError(
1418
+ "INVALID_TRANSACTION_PLAN",
1419
+ "stakeRounding.enabled must be a boolean."
1420
+ );
1421
+ }
1422
+ if (!options || !options.enabled)
1423
+ return prepareSubtensorToEvmExact(provider, input);
1424
+ if (input.source !== "staked" || !input.stakePulls?.length) {
1425
+ throw new ForeverMoneyError(
1426
+ "INVALID_TRANSACTION_PLAN",
1427
+ "Stake rounding requires a staked source with named stakePulls."
1428
+ );
1429
+ }
1430
+ const minimum = minimumSubtensorOutput(input);
1431
+ const result = await estimateRoundedStake({
1432
+ amountWei: input.amountWei,
1433
+ minAmountOutWei: minimum,
1434
+ pulls: input.stakePulls,
1435
+ positions: options.positions,
1436
+ minStakeRao: options.minStakeRao ?? 0n,
1437
+ partnerFeeBps: input.partnerFee?.bps ?? 0,
1438
+ quoteAndEstimate: async (amountWei, stakePulls, minAmountOutWei) => ({
1439
+ prepared: await prepareSubtensorToEvmExact(provider, {
1440
+ ...input,
1441
+ amountWei,
1442
+ stakePulls,
1443
+ minAmountOutWei,
1444
+ stakeRounding: false
1445
+ })
1446
+ })
1447
+ });
1448
+ return Object.freeze({
1449
+ ...result.prepared,
1450
+ stakeRounding: Object.freeze({
1451
+ requestedAmountWei: input.amountWei,
1452
+ amountWei: result.amountWei,
1453
+ minAmountOutWei: result.minAmountOutWei,
1454
+ pulls: result.pulls,
1455
+ simulationComplete: !result.prepared.plan.steps.some(
1456
+ (step2) => step2.kind === "approval"
1457
+ )
1458
+ })
1459
+ });
1460
+ }
1252
1461
  function prepareSubtensorToBase(provider, input) {
1253
1462
  return prepareSubtensorToEvm(provider, { ...input, evmChain: "base" });
1254
1463
  }