@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.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)",
@@ -358,7 +572,9 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
358
572
  "function maxIntegratorFeeBps() view returns (uint16)",
359
573
  "function bridgeFeeBps() view returns (uint16)",
360
574
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
575
+ "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit) view returns (uint256 fee)",
361
576
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
577
+ "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit,uint256 gasLimit) payable returns (bytes32 messageId)",
362
578
  "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)",
363
579
  "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)",
364
580
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
@@ -376,7 +592,6 @@ var ALPHA_GATEWAY_ABI = Object.freeze([
376
592
  "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
377
593
  "function bridgeOutFromValidators(uint64 destSelector,address token,address recipient,uint256 taoAmount,(bytes32 validator,uint256 alphaRao)[] sources,uint256 minTokenOut) payable returns (bytes32 messageId)",
378
594
  "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)",
379
- "function minStakeRequired() view returns (uint256)",
380
595
  "function MAX_STAKE_SOURCES() view returns (uint256)",
381
596
  "function GATEWAY_COLDKEY() view returns (bytes32)",
382
597
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
@@ -455,89 +670,6 @@ function createTransactionPlan(plan) {
455
670
  });
456
671
  }
457
672
 
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
673
  // src/bridge/plans.ts
542
674
  var erc20Abi = parseAbi(ERC20_ABI);
543
675
  var spokeAbi = parseAbi(SPOKE_GATEWAY_ABI);
@@ -546,6 +678,18 @@ var stakingAbi = parseAbi(STAKING_ABI);
546
678
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
547
679
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
548
680
  var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
681
+ var EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT = 3500000n;
682
+ function resolveDestinationGasLimit(value) {
683
+ const gasLimit = value ?? EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT;
684
+ if (typeof gasLimit !== "bigint" || gasLimit <= 0n) {
685
+ throw new ForeverMoneyError(
686
+ "INVALID_TRANSACTION_PLAN",
687
+ "Destination gas limit must be a positive bigint.",
688
+ { destinationGasLimit: String(gasLimit) }
689
+ );
690
+ }
691
+ return gasLimit;
692
+ }
549
693
  var MAX_PARTNER_FEE_BPS = 1e4;
550
694
  var MAX_STAKE_PULLS = 16;
551
695
  var NO_PARTNER_FEE = {
@@ -722,15 +866,15 @@ function partnerFeeSummary(charged, unit, fee) {
722
866
  if (fee.bps === 0) return "";
723
867
  return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
724
868
  }
725
- function encodeSpokeBridge(token, amountWei, exit, fee) {
869
+ function encodeSpokeBridge(token, amountWei, exit, destinationGasLimit, fee) {
726
870
  return fee.bps === 0 ? encodeFunctionData({
727
871
  abi: spokeAbi,
728
872
  functionName: "bridgeToFinney",
729
- args: [token, amountWei, exit]
873
+ args: [token, amountWei, exit, destinationGasLimit]
730
874
  }) : encodeFunctionData({
731
875
  abi: spokeAbi,
732
876
  functionName: "bridgeToFinneyWithFee",
733
- args: [token, amountWei, exit, 0n, fee]
877
+ args: [token, amountWei, exit, destinationGasLimit, fee]
734
878
  });
735
879
  }
736
880
  function resolveStakePulls(pulls, amountRao) {
@@ -817,7 +961,7 @@ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaR
817
961
  args: [...args, fee]
818
962
  });
819
963
  }
820
- async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
964
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, destinationGasLimit, fee) {
821
965
  const maxBps = await provider.readContract({
822
966
  address: gateway,
823
967
  abi: spokeAbi,
@@ -828,7 +972,7 @@ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee)
828
972
  address: gateway,
829
973
  abi: spokeAbi,
830
974
  functionName: "quoteBridgeToFinneyWithFee",
831
- args: [token, amountWei, exit, 0n, fee]
975
+ args: [token, amountWei, exit, destinationGasLimit, fee]
832
976
  });
833
977
  return networkFee;
834
978
  }
@@ -874,6 +1018,9 @@ function buildEvmToSubtensorPlan(input) {
874
1018
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
875
1019
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
876
1020
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
1021
+ const destinationGasLimit = resolveDestinationGasLimit(
1022
+ input.destinationGasLimit
1023
+ );
877
1024
  const destination = normalizeSS58(input.destination);
878
1025
  const exit = {
879
1026
  ss58: ss58ToPublicKey(destination),
@@ -917,6 +1064,7 @@ function buildEvmToSubtensorPlan(input) {
917
1064
  asset.evmToken,
918
1065
  input.amountWei,
919
1066
  exit,
1067
+ destinationGasLimit,
920
1068
  partnerFee
921
1069
  ),
922
1070
  value,
@@ -1032,6 +1180,9 @@ async function prepareEvmToSubtensor(provider, input) {
1032
1180
  assertDelivery(input.delivery);
1033
1181
  assertAssetMode(input.asset, input.delivery);
1034
1182
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
1183
+ const destinationGasLimit = resolveDestinationGasLimit(
1184
+ input.destinationGasLimit
1185
+ );
1035
1186
  const destination = normalizeSS58(input.destination);
1036
1187
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1037
1188
  const exit = {
@@ -1056,13 +1207,19 @@ async function prepareEvmToSubtensor(provider, input) {
1056
1207
  address: evm.contracts.gateway,
1057
1208
  abi: spokeAbi,
1058
1209
  functionName: "quoteBridgeToFinney",
1059
- args: [asset.evmToken, input.amountWei, exit]
1210
+ args: [
1211
+ asset.evmToken,
1212
+ input.amountWei,
1213
+ exit,
1214
+ destinationGasLimit
1215
+ ]
1060
1216
  }) : quoteSpokeWithFee(
1061
1217
  provider,
1062
1218
  evm.contracts.gateway,
1063
1219
  asset.evmToken,
1064
1220
  input.amountWei,
1065
1221
  exit,
1222
+ destinationGasLimit,
1066
1223
  partnerFee
1067
1224
  )
1068
1225
  ]);
@@ -1072,6 +1229,7 @@ async function prepareEvmToSubtensor(provider, input) {
1072
1229
  asset.evmToken,
1073
1230
  input.amountWei,
1074
1231
  exit,
1232
+ destinationGasLimit,
1075
1233
  partnerFee
1076
1234
  );
1077
1235
  estimatedBridgeGas = await provider.estimateGas({
@@ -1097,7 +1255,7 @@ async function prepareEvmToSubtensor(provider, input) {
1097
1255
  function prepareBaseToSubtensor(provider, input) {
1098
1256
  return prepareEvmToSubtensor(provider, { ...input, evmChain: "base" });
1099
1257
  }
1100
- async function prepareSubtensorToEvm(provider, input) {
1258
+ async function prepareSubtensorToEvmExact(provider, input) {
1101
1259
  const sender = normalizeEvmAddress(input.sender);
1102
1260
  const recipient = normalizeEvmAddress(input.recipient);
1103
1261
  const asset = bridgeAsset(input.evmChain, input.asset);
@@ -1179,6 +1337,53 @@ async function prepareSubtensorToEvm(provider, input) {
1179
1337
  plan
1180
1338
  });
1181
1339
  }
1340
+ async function prepareSubtensorToEvm(provider, input) {
1341
+ const options = input.stakeRounding;
1342
+ if (options != null && options !== false && typeof options.enabled !== "boolean") {
1343
+ throw new ForeverMoneyError(
1344
+ "INVALID_TRANSACTION_PLAN",
1345
+ "stakeRounding.enabled must be a boolean."
1346
+ );
1347
+ }
1348
+ if (!options || !options.enabled)
1349
+ return prepareSubtensorToEvmExact(provider, input);
1350
+ if (input.source !== "staked" || !input.stakePulls?.length) {
1351
+ throw new ForeverMoneyError(
1352
+ "INVALID_TRANSACTION_PLAN",
1353
+ "Stake rounding requires a staked source with named stakePulls."
1354
+ );
1355
+ }
1356
+ const minimum = minimumSubtensorOutput(input);
1357
+ const result = await estimateRoundedStake({
1358
+ amountWei: input.amountWei,
1359
+ minAmountOutWei: minimum,
1360
+ pulls: input.stakePulls,
1361
+ positions: options.positions,
1362
+ minStakeRao: options.minStakeRao ?? 0n,
1363
+ partnerFeeBps: input.partnerFee?.bps ?? 0,
1364
+ quoteAndEstimate: async (amountWei, stakePulls, minAmountOutWei) => ({
1365
+ prepared: await prepareSubtensorToEvmExact(provider, {
1366
+ ...input,
1367
+ amountWei,
1368
+ stakePulls,
1369
+ minAmountOutWei,
1370
+ stakeRounding: false
1371
+ })
1372
+ })
1373
+ });
1374
+ return Object.freeze({
1375
+ ...result.prepared,
1376
+ stakeRounding: Object.freeze({
1377
+ requestedAmountWei: input.amountWei,
1378
+ amountWei: result.amountWei,
1379
+ minAmountOutWei: result.minAmountOutWei,
1380
+ pulls: result.pulls,
1381
+ simulationComplete: !result.prepared.plan.steps.some(
1382
+ (step2) => step2.kind === "approval"
1383
+ )
1384
+ })
1385
+ });
1386
+ }
1182
1387
  function prepareSubtensorToBase(provider, input) {
1183
1388
  return prepareSubtensorToEvm(provider, { ...input, evmChain: "base" });
1184
1389
  }
@@ -2240,6 +2445,7 @@ export {
2240
2445
  BASE_CCIP_SELECTOR,
2241
2446
  BASE_CHAIN_ID,
2242
2447
  BASIS_POINTS,
2448
+ EVM_TO_SUBTENSOR_DESTINATION_GAS_LIMIT,
2243
2449
  EVM_WEI_PER_RAO,
2244
2450
  FOREVERMONEY_DEPLOYMENT_VERSION,
2245
2451
  ForeverMoneyError,
@@ -2269,6 +2475,7 @@ export {
2269
2475
  buildWithdrawVaultPlan,
2270
2476
  createForeverMoneyClient,
2271
2477
  destinationChainId,
2478
+ estimateRoundedStake,
2272
2479
  evmChainFromBridgeDirection,
2273
2480
  evmToMirrorSS58,
2274
2481
  feeWithBuffer,
@@ -2290,6 +2497,8 @@ export {
2290
2497
  partnerFeeTaoTopUp,
2291
2498
  sourceChainId,
2292
2499
  ss58ToPublicKey,
2500
+ stakedMinimumOutput,
2501
+ strandedStakeSource,
2293
2502
  toEip1193Transaction,
2294
2503
  toEthersTransaction,
2295
2504
  toViemTransaction,