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