@forevermoney/sdk 0.4.0 → 0.5.1

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 — 2026-09-16
4
+
5
+ - Added optional `minAmountOutWei` to Subtensor-to-EVM bridge requests. Callers can explicitly bound output slippage, including staking rounding dust. Defaults to the input amount; rejects zero, negative, non-bigint, and above-input minima before RPC calls. The same minimum is used for gas estimation and final calldata, for single- and multi-validator routes with or without partner fees.
6
+
7
+ ## 0.5.0 — 2026-09-16
8
+
9
+ - Staked bridges from Subtensor can pull from several validators. Pass
10
+ `stakePulls: [{ hotkey, amountRao }]` (1–16 entries, unique hotkeys, summing
11
+ to the bridged amount) and the plan calls the V5.1 gateway's
12
+ `bridgeOutFromValidators` (or `…WithFee`), which re-delegates every pull to
13
+ the token's canonical validator before depositing. Without `stakePulls` the
14
+ plan keeps the single-validator `bridgeOut` call. With a partner fee the cut
15
+ is charged on top and spread across the pulls in proportion to their
16
+ amounts, so each position needs headroom for its share. Added `StakePull`, `MAX_STAKE_PULLS`, and the
17
+ `bridgeOutFromValidators*`, `minStakeRequired`, `MAX_STAKE_SOURCES` and
18
+ `GATEWAY_COLDKEY` ABI entries.
19
+ - Moved the Subtensor gateway to the V5.1 deployment at
20
+ `0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69`. `contracts.legacyGateway` is
21
+ now `contracts.legacyGateways`, a list of retired hubs (oldest first) used for
22
+ receipt and delivery tracking; the V5 hub stays listed because the spoke
23
+ gateways still deliver through it. Deployment metadata version is 1.3.0.
24
+
3
25
  ## 0.4.0 — 2026-09-16
4
26
 
5
27
  - Added `asset: 'sn80'` to the bridge builders and preparation methods for Base
package/README.md CHANGED
@@ -11,9 +11,12 @@ is transferred 1:1 and the network fee is charged separately, so the SDK fixes
11
11
  the contract's minimum destination output to the bridged principal instead of
12
12
  exposing configurable slippage.
13
13
 
14
- Bridge plans use the V5 gateways in `contracts.gateway`. The previous gateways
15
- remain under `contracts.legacyGateway` only so receipts and deliveries of
16
- bridges sent through them can still be tracked. Plans call the zero-fee V5
14
+ Bridge plans use the current gateways in `contracts.gateway`. Retired gateways
15
+ remain under `contracts.legacyGateways` only so receipts and deliveries of
16
+ bridges sent through them can still be tracked. A spoke gateway's Subtensor
17
+ hub pointer is immutable, so after a hub-only redeploy EVM-to-Subtensor
18
+ deliveries still arrive through the previous hub; the SDK watches every known
19
+ hub, so tracking and recovery detection are unaffected. Plans call the zero-fee V5
17
20
  entrypoints unless a `partnerFee` is passed (see "Charge a partner fee").
18
21
 
19
22
  ## Install
@@ -172,6 +175,36 @@ Canonical SN80 token addresses are exported as `contracts.wrappedSn80`:
172
175
  The Base address `0x2292233d308188fcb3775f63a20f31dff6db02d9` is the SN80/TAO
173
176
  liquidity pool; bridge calls use the token addresses above.
174
177
 
178
+ ## Bridge stake held with several validators
179
+
180
+ Staked alpha on Finney is keyed by validator hotkey, and a bridge can only pull
181
+ from the positions you name. Pass `stakePulls` to draw from more than one
182
+ validator; the gateway re-delegates each pull to the token's canonical
183
+ validator and deposits the total, so stake with any validator can be bridged.
184
+
185
+ ```ts
186
+ const prepared = await foreverMoney.bridge.prepareSubtensorToBase({
187
+ sender: '0x...',
188
+ recipient: '0x...',
189
+ amountWei: 140n * 10n ** 18n,
190
+ source: 'staked',
191
+ netuid: 0n,
192
+ stakePulls: [
193
+ { hotkey: '0x…validatorA', amountRao: 100_000_000_000n },
194
+ { hotkey: '0x…validatorB', amountRao: 40_000_000_000n },
195
+ ],
196
+ })
197
+ ```
198
+
199
+ Rules the SDK checks before building a plan: 1 to `MAX_STAKE_PULLS` (16)
200
+ entries, unique non-zero hotkeys, positive amounts, and a sum equal to the
201
+ bridged amount in RAO. One staking approval on the netuid covers every pull.
202
+ Nothing on-chain enforces the runtime's minimum stake on what you leave behind
203
+ (`minStakeRequired()`, 0.02 TAO-equivalent), so size each pull to either drain
204
+ the position or leave at least that much. With a partner fee the cut is charged
205
+ on top and spread across the pulls in proportion to their amounts, so each
206
+ position needs headroom for its share.
207
+
175
208
  ## Charge a partner fee
176
209
 
177
210
  Integrators can take a fee on each bridge by passing `partnerFee` to any bridge
@@ -405,3 +438,7 @@ testing are documented in [`docs/testing.md`](./docs/testing.md).
405
438
 
406
439
  Security reports should follow [`SECURITY.md`](./SECURITY.md). Maintainer release
407
440
  steps are in [`docs/releasing.md`](./docs/releasing.md).
441
+
442
+ ### Minimum bridge output
443
+
444
+ Subtensor-to-EVM builders and preparation methods accept optional `minAmountOutWei` (destination token units, 18 decimals, after partner fees). It must be positive and no greater than `amountWei`; omission preserves the exact-output default. Choose the minimum explicitly to cover your acceptable slippage or native staking rounding dust. The SDK uses the same value for gas estimation and final transaction calldata. For example, `minAmountOutWei: amountWei - 4n * EVM_WEI_PER_RAO` allows four native RAO of dust when the amount exceeds that budget. This does not change the input amount or approval amount.
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  ForeverMoneyError: () => ForeverMoneyError,
29
29
  GAS_LIMIT_BUFFER_BPS: () => GAS_LIMIT_BUFFER_BPS,
30
30
  MAX_PARTNER_FEE_BPS: () => MAX_PARTNER_FEE_BPS,
31
+ MAX_STAKE_PULLS: () => MAX_STAKE_PULLS,
31
32
  MIN_LIQUID_BASE_TO_SUBTENSOR_WEI: () => MIN_LIQUID_BASE_TO_SUBTENSOR_WEI,
32
33
  MIN_LIQUID_EVM_TO_SUBTENSOR_WEI: () => MIN_LIQUID_EVM_TO_SUBTENSOR_WEI,
33
34
  MIN_LIQUID_SUBTENSOR_TO_EVM_WEI: () => MIN_LIQUID_SUBTENSOR_TO_EVM_WEI,
@@ -162,7 +163,7 @@ var SUBTENSOR_CCIP_SELECTOR = 2135107236357186872n;
162
163
  var RAO_PER_TAO = 1000000000n;
163
164
  var EVM_WEI_PER_RAO = 1000000000n;
164
165
  var SN80_NETUID = 80n;
165
- var FOREVERMONEY_DEPLOYMENT_VERSION = "1.2.0";
166
+ var FOREVERMONEY_DEPLOYMENT_VERSION = "1.3.0";
166
167
  var foreverMoneyDeployment = Object.freeze({
167
168
  version: FOREVERMONEY_DEPLOYMENT_VERSION,
168
169
  base: Object.freeze({
@@ -172,9 +173,11 @@ var foreverMoneyDeployment = Object.freeze({
172
173
  ccipSelector: BASE_CCIP_SELECTOR,
173
174
  contracts: Object.freeze({
174
175
  gateway: (0, import_viem2.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
175
- legacyGateway: (0, import_viem2.getAddress)(
176
- "0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"
177
- ),
176
+ // Retired gateways, oldest first; kept only to track transfers sent
177
+ // through them.
178
+ legacyGateways: [
179
+ (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
180
+ ],
178
181
  wrappedTao: (0, import_viem2.getAddress)(
179
182
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
180
183
  ),
@@ -207,9 +210,9 @@ var foreverMoneyDeployment = Object.freeze({
207
210
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
208
211
  contracts: Object.freeze({
209
212
  gateway: (0, import_viem2.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
210
- legacyGateway: (0, import_viem2.getAddress)(
211
- "0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"
212
- ),
213
+ legacyGateways: [
214
+ (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
215
+ ],
213
216
  wrappedTao: (0, import_viem2.getAddress)(
214
217
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
215
218
  ),
@@ -225,10 +228,14 @@ var foreverMoneyDeployment = Object.freeze({
225
228
  chainId: SUBTENSOR_CHAIN_ID,
226
229
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
227
230
  contracts: Object.freeze({
228
- gateway: (0, import_viem2.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787"),
229
- legacyGateway: (0, import_viem2.getAddress)(
230
- "0x998f20Fea90bF7792774dECc7f994716442B1705"
231
- ),
231
+ // V5.1 AlphaGateway: multi-validator staked input (tao-bridge 485c898).
232
+ gateway: (0, import_viem2.getAddress)("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
233
+ // V4 and V5 hubs. The spokes still deliver to the V5 hub (their hub
234
+ // pointer is immutable), so it stays here for delivery tracking.
235
+ legacyGateways: [
236
+ (0, import_viem2.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
237
+ (0, import_viem2.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
238
+ ],
232
239
  alphaVault: (0, import_viem2.getAddress)(
233
240
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
234
241
  ),
@@ -437,6 +444,11 @@ var ALPHA_GATEWAY_ABI = Object.freeze([
437
444
  "function integratorCut(uint256 amount,uint16 bps) pure returns (uint256)",
438
445
  "function quoteBridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 mintedAmount,uint256 taoAmount,uint256 stakedAlphaRao,(address recipient,uint16 bps) integrator) view returns (uint256 fee,uint256 nativeTopUp,uint256 alphaTopUp,uint256 amountCrossing)",
439
446
  "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
447
+ "function bridgeOutFromValidators(uint64 destSelector,address token,address recipient,uint256 taoAmount,(bytes32 validator,uint256 alphaRao)[] sources,uint256 minTokenOut) payable returns (bytes32 messageId)",
448
+ "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
+ "function MAX_STAKE_SOURCES() view returns (uint256)",
451
+ "function GATEWAY_COLDKEY() view returns (bytes32)",
440
452
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
441
453
  "function claimNative(address to)",
442
454
  "function claimStaked(address token,bytes32 destColdkey,address to)",
@@ -605,6 +617,7 @@ var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
605
617
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
606
618
  var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
607
619
  var MAX_PARTNER_FEE_BPS = 1e4;
620
+ var MAX_STAKE_PULLS = 16;
608
621
  var NO_PARTNER_FEE = {
609
622
  recipient: "0x0000000000000000000000000000000000000000",
610
623
  bps: 0
@@ -698,6 +711,27 @@ function sourceNetuid(input) {
698
711
  }
699
712
  return netuid;
700
713
  }
714
+ function stakePullsFor(input, amountRao) {
715
+ if (input.stakePulls === void 0) return void 0;
716
+ if (input.source !== "staked") {
717
+ throw new ForeverMoneyError(
718
+ "INVALID_TRANSACTION_PLAN",
719
+ "Stake pulls are only valid for a staked source."
720
+ );
721
+ }
722
+ return resolveStakePulls(input.stakePulls, amountRao);
723
+ }
724
+ function minimumSubtensorOutput(input) {
725
+ const minimum = input.minAmountOutWei ?? input.amountWei;
726
+ assertNonNegativeAmount(minimum, "Minimum output");
727
+ if (minimum === 0n || minimum > input.amountWei) {
728
+ throw new ForeverMoneyError(
729
+ "INVALID_TRANSACTION_PLAN",
730
+ "Minimum output must be positive and no greater than the input amount."
731
+ );
732
+ }
733
+ return minimum;
734
+ }
701
735
  function assertDelivery(value) {
702
736
  if (value !== "liquid" && value !== "staked") {
703
737
  throw new ForeverMoneyError(
@@ -769,7 +803,72 @@ function encodeSpokeBridge(token, amountWei, exit, fee) {
769
803
  args: [token, amountWei, exit, 0n, fee]
770
804
  });
771
805
  }
772
- function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee) {
806
+ function resolveStakePulls(pulls, amountRao) {
807
+ if (pulls.length === 0 || pulls.length > MAX_STAKE_PULLS) {
808
+ throw new ForeverMoneyError(
809
+ "INVALID_TRANSACTION_PLAN",
810
+ `Between 1 and ${MAX_STAKE_PULLS} stake pulls are required.`,
811
+ { count: pulls.length }
812
+ );
813
+ }
814
+ const seen = /* @__PURE__ */ new Set();
815
+ let total = 0n;
816
+ const resolved = pulls.map((pull) => {
817
+ const validator = normalizeBytes32(pull.hotkey, "Stake pull hotkey");
818
+ if (validator === `0x${"0".repeat(64)}`) {
819
+ throw new ForeverMoneyError(
820
+ "INVALID_TRANSACTION_PLAN",
821
+ "Stake pull hotkey must not be zero."
822
+ );
823
+ }
824
+ const key = validator.toLowerCase();
825
+ if (seen.has(key)) {
826
+ throw new ForeverMoneyError(
827
+ "INVALID_TRANSACTION_PLAN",
828
+ "Stake pulls must not repeat a hotkey.",
829
+ { hotkey: validator }
830
+ );
831
+ }
832
+ seen.add(key);
833
+ if (typeof pull.amountRao !== "bigint" || pull.amountRao <= 0n) {
834
+ throw new ForeverMoneyError(
835
+ "INVALID_TRANSACTION_PLAN",
836
+ "Each stake pull must take a positive amount of RAO.",
837
+ { hotkey: validator }
838
+ );
839
+ }
840
+ total += pull.amountRao;
841
+ return { validator, alphaRao: pull.amountRao };
842
+ });
843
+ if (total !== amountRao) {
844
+ throw new ForeverMoneyError(
845
+ "INVALID_TRANSACTION_PLAN",
846
+ "Stake pulls must sum to the bridged amount.",
847
+ { pulledRao: total.toString(), amountRao: amountRao.toString() }
848
+ );
849
+ }
850
+ return resolved;
851
+ }
852
+ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee, pulls) {
853
+ if (pulls !== void 0) {
854
+ const args2 = [
855
+ destSelector,
856
+ token,
857
+ recipient,
858
+ taoAmount,
859
+ pulls,
860
+ minTokenOut
861
+ ];
862
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
863
+ abi: alphaAbi,
864
+ functionName: "bridgeOutFromValidators",
865
+ args: args2
866
+ }) : (0, import_viem6.encodeFunctionData)({
867
+ abi: alphaAbi,
868
+ functionName: "bridgeOutFromValidatorsWithFee",
869
+ args: [...args2, fee]
870
+ });
871
+ }
773
872
  const args = [
774
873
  destSelector,
775
874
  token,
@@ -911,6 +1010,7 @@ function buildSubtensorToEvmPlan(input) {
911
1010
  assertAssetMode(input.asset, input.source);
912
1011
  const netuid = sourceNetuid(input);
913
1012
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1013
+ const minAmountOutWei = minimumSubtensorOutput(input);
914
1014
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
915
1015
  const { subtensor } = foreverMoneyDeployment;
916
1016
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -959,6 +1059,7 @@ function buildSubtensorToEvmPlan(input) {
959
1059
  }
960
1060
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
961
1061
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
1062
+ const pulls = stakePullsFor(input, amountRao);
962
1063
  const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
963
1064
  assertNonNegativeAmount(value, "Transaction value");
964
1065
  steps.push(
@@ -974,8 +1075,9 @@ function buildSubtensorToEvmPlan(input) {
974
1075
  recipient,
975
1076
  taoAmount,
976
1077
  stakedAlphaRao,
977
- input.amountWei,
978
- partnerFee
1078
+ minAmountOutWei,
1079
+ partnerFee,
1080
+ pulls
979
1081
  ),
980
1082
  value,
981
1083
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
@@ -1073,6 +1175,7 @@ async function prepareSubtensorToEvm(provider, input) {
1073
1175
  assertAssetMode(input.asset, input.source);
1074
1176
  const netuid = sourceNetuid(input);
1075
1177
  assertSubtensorToEvmAmount(input.amountWei, input.source);
1178
+ const minAmountOutWei = minimumSubtensorOutput(input);
1076
1179
  const { subtensor } = foreverMoneyDeployment;
1077
1180
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1078
1181
  const partnerFee = resolvePartnerFee(
@@ -1126,8 +1229,9 @@ async function prepareSubtensorToEvm(provider, input) {
1126
1229
  recipient,
1127
1230
  taoAmount,
1128
1231
  stakedAlphaRao,
1129
- input.amountWei,
1130
- partnerFee
1232
+ minAmountOutWei,
1233
+ partnerFee,
1234
+ stakePullsFor(input, stakedAlphaRao)
1131
1235
  ),
1132
1236
  value
1133
1237
  });
@@ -1730,12 +1834,12 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1730
1834
  );
1731
1835
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1732
1836
  const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1733
- [evm.contracts.legacyGateway, evm.contracts.gateway],
1837
+ [...evm.contracts.legacyGateways, evm.contracts.gateway],
1734
1838
  spokeGatewayAbi,
1735
1839
  "BridgedToFinney"
1736
1840
  ] : [
1737
1841
  [
1738
- foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1842
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1739
1843
  foreverMoneyDeployment.subtensor.contracts.gateway
1740
1844
  ],
1741
1845
  alphaGatewayAbi,
@@ -1897,7 +2001,7 @@ async function getCcipDeliveryStatus(provider, input) {
1897
2001
  );
1898
2002
  }
1899
2003
  const subtensorGateways = [
1900
- foreverMoneyDeployment.subtensor.contracts.legacyGateway,
2004
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1901
2005
  foreverMoneyDeployment.subtensor.contracts.gateway
1902
2006
  ];
1903
2007
  for (const log of receipt.logs) {