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