@forevermoney/sdk 0.4.0 → 0.5.0

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-09-16
4
+
5
+ - Staked bridges from Subtensor can pull from several validators. Pass
6
+ `stakePulls: [{ hotkey, amountRao }]` (1–16 entries, unique hotkeys, summing
7
+ to the bridged amount) and the plan calls the V5.1 gateway's
8
+ `bridgeOutFromValidators` (or `…WithFee`), which re-delegates every pull to
9
+ the token's canonical validator before depositing. Without `stakePulls` the
10
+ plan keeps the single-validator `bridgeOut` call. With a partner fee the cut
11
+ is charged on top and spread across the pulls in proportion to their
12
+ amounts, so each position needs headroom for its share. Added `StakePull`, `MAX_STAKE_PULLS`, and the
13
+ `bridgeOutFromValidators*`, `minStakeRequired`, `MAX_STAKE_SOURCES` and
14
+ `GATEWAY_COLDKEY` ABI entries.
15
+ - Moved the Subtensor gateway to the V5.1 deployment at
16
+ `0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69`. `contracts.legacyGateway` is
17
+ now `contracts.legacyGateways`, a list of retired hubs (oldest first) used for
18
+ receipt and delivery tracking; the V5 hub stays listed because the spoke
19
+ gateways still deliver through it. Deployment metadata version is 1.3.0.
20
+
3
21
  ## 0.4.0 — 2026-09-16
4
22
 
5
23
  - 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
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,16 @@ 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
+ }
701
724
  function assertDelivery(value) {
702
725
  if (value !== "liquid" && value !== "staked") {
703
726
  throw new ForeverMoneyError(
@@ -769,7 +792,72 @@ function encodeSpokeBridge(token, amountWei, exit, fee) {
769
792
  args: [token, amountWei, exit, 0n, fee]
770
793
  });
771
794
  }
772
- function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee) {
795
+ function resolveStakePulls(pulls, amountRao) {
796
+ if (pulls.length === 0 || pulls.length > MAX_STAKE_PULLS) {
797
+ throw new ForeverMoneyError(
798
+ "INVALID_TRANSACTION_PLAN",
799
+ `Between 1 and ${MAX_STAKE_PULLS} stake pulls are required.`,
800
+ { count: pulls.length }
801
+ );
802
+ }
803
+ const seen = /* @__PURE__ */ new Set();
804
+ let total = 0n;
805
+ const resolved = pulls.map((pull) => {
806
+ const validator = normalizeBytes32(pull.hotkey, "Stake pull hotkey");
807
+ if (validator === `0x${"0".repeat(64)}`) {
808
+ throw new ForeverMoneyError(
809
+ "INVALID_TRANSACTION_PLAN",
810
+ "Stake pull hotkey must not be zero."
811
+ );
812
+ }
813
+ const key = validator.toLowerCase();
814
+ if (seen.has(key)) {
815
+ throw new ForeverMoneyError(
816
+ "INVALID_TRANSACTION_PLAN",
817
+ "Stake pulls must not repeat a hotkey.",
818
+ { hotkey: validator }
819
+ );
820
+ }
821
+ seen.add(key);
822
+ if (typeof pull.amountRao !== "bigint" || pull.amountRao <= 0n) {
823
+ throw new ForeverMoneyError(
824
+ "INVALID_TRANSACTION_PLAN",
825
+ "Each stake pull must take a positive amount of RAO.",
826
+ { hotkey: validator }
827
+ );
828
+ }
829
+ total += pull.amountRao;
830
+ return { validator, alphaRao: pull.amountRao };
831
+ });
832
+ if (total !== amountRao) {
833
+ throw new ForeverMoneyError(
834
+ "INVALID_TRANSACTION_PLAN",
835
+ "Stake pulls must sum to the bridged amount.",
836
+ { pulledRao: total.toString(), amountRao: amountRao.toString() }
837
+ );
838
+ }
839
+ return resolved;
840
+ }
841
+ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee, pulls) {
842
+ if (pulls !== void 0) {
843
+ const args2 = [
844
+ destSelector,
845
+ token,
846
+ recipient,
847
+ taoAmount,
848
+ pulls,
849
+ minTokenOut
850
+ ];
851
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
852
+ abi: alphaAbi,
853
+ functionName: "bridgeOutFromValidators",
854
+ args: args2
855
+ }) : (0, import_viem6.encodeFunctionData)({
856
+ abi: alphaAbi,
857
+ functionName: "bridgeOutFromValidatorsWithFee",
858
+ args: [...args2, fee]
859
+ });
860
+ }
773
861
  const args = [
774
862
  destSelector,
775
863
  token,
@@ -959,6 +1047,7 @@ function buildSubtensorToEvmPlan(input) {
959
1047
  }
960
1048
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
961
1049
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
1050
+ const pulls = stakePullsFor(input, amountRao);
962
1051
  const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
963
1052
  assertNonNegativeAmount(value, "Transaction value");
964
1053
  steps.push(
@@ -975,7 +1064,8 @@ function buildSubtensorToEvmPlan(input) {
975
1064
  taoAmount,
976
1065
  stakedAlphaRao,
977
1066
  input.amountWei,
978
- partnerFee
1067
+ partnerFee,
1068
+ pulls
979
1069
  ),
980
1070
  value,
981
1071
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
@@ -1127,7 +1217,8 @@ async function prepareSubtensorToEvm(provider, input) {
1127
1217
  taoAmount,
1128
1218
  stakedAlphaRao,
1129
1219
  input.amountWei,
1130
- partnerFee
1220
+ partnerFee,
1221
+ stakePullsFor(input, stakedAlphaRao)
1131
1222
  ),
1132
1223
  value
1133
1224
  });
@@ -1730,12 +1821,12 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1730
1821
  );
1731
1822
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1732
1823
  const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1733
- [evm.contracts.legacyGateway, evm.contracts.gateway],
1824
+ [...evm.contracts.legacyGateways, evm.contracts.gateway],
1734
1825
  spokeGatewayAbi,
1735
1826
  "BridgedToFinney"
1736
1827
  ] : [
1737
1828
  [
1738
- foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1829
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1739
1830
  foreverMoneyDeployment.subtensor.contracts.gateway
1740
1831
  ],
1741
1832
  alphaGatewayAbi,
@@ -1897,7 +1988,7 @@ async function getCcipDeliveryStatus(provider, input) {
1897
1988
  );
1898
1989
  }
1899
1990
  const subtensorGateways = [
1900
- foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1991
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1901
1992
  foreverMoneyDeployment.subtensor.contracts.gateway
1902
1993
  ];
1903
1994
  for (const log of receipt.logs) {