@forevermoney/sdk 0.2.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/dist/index.js CHANGED
@@ -92,7 +92,8 @@ var ROBINHOOD_CCIP_SELECTOR = 6180753054346818345n;
92
92
  var SUBTENSOR_CCIP_SELECTOR = 2135107236357186872n;
93
93
  var RAO_PER_TAO = 1000000000n;
94
94
  var EVM_WEI_PER_RAO = 1000000000n;
95
- var FOREVERMONEY_DEPLOYMENT_VERSION = "1.1.0";
95
+ var SN80_NETUID = 80n;
96
+ var FOREVERMONEY_DEPLOYMENT_VERSION = "1.3.0";
96
97
  var foreverMoneyDeployment = Object.freeze({
97
98
  version: FOREVERMONEY_DEPLOYMENT_VERSION,
98
99
  base: Object.freeze({
@@ -101,10 +102,18 @@ var foreverMoneyDeployment = Object.freeze({
101
102
  chainId: BASE_CHAIN_ID,
102
103
  ccipSelector: BASE_CCIP_SELECTOR,
103
104
  contracts: Object.freeze({
104
- gateway: getAddress2("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"),
105
+ gateway: getAddress2("0x1da2415229b614C787e145D1D7346eb496319C52"),
106
+ // Retired gateways, oldest first; kept only to track transfers sent
107
+ // through them.
108
+ legacyGateways: [
109
+ getAddress2("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
110
+ ],
105
111
  wrappedTao: getAddress2(
106
112
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
107
113
  ),
114
+ wrappedSn80: getAddress2(
115
+ "0x6f63d869011f95274498023b4abfc00b30c34378"
116
+ ),
108
117
  ccipRouter: getAddress2(
109
118
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
110
119
  ),
@@ -130,7 +139,10 @@ var foreverMoneyDeployment = Object.freeze({
130
139
  chainId: ROBINHOOD_CHAIN_ID,
131
140
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
132
141
  contracts: Object.freeze({
133
- gateway: getAddress2("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"),
142
+ gateway: getAddress2("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
143
+ legacyGateways: [
144
+ getAddress2("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
145
+ ],
134
146
  wrappedTao: getAddress2(
135
147
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
136
148
  ),
@@ -146,13 +158,23 @@ var foreverMoneyDeployment = Object.freeze({
146
158
  chainId: SUBTENSOR_CHAIN_ID,
147
159
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
148
160
  contracts: Object.freeze({
149
- gateway: getAddress2("0x998f20Fea90bF7792774dECc7f994716442B1705"),
161
+ // V5.1 AlphaGateway: multi-validator staked input (tao-bridge 485c898).
162
+ gateway: getAddress2("0xd5Fa238aa4177f6c1341491969d9cBeec94EEd69"),
163
+ // V4 and V5 hubs. The spokes still deliver to the V5 hub (their hub
164
+ // pointer is immutable), so it stays here for delivery tracking.
165
+ legacyGateways: [
166
+ getAddress2("0x998f20Fea90bF7792774dECc7f994716442B1705"),
167
+ getAddress2("0xcd0C6d98D0A126B1c113d15b4c28F38321437787")
168
+ ],
150
169
  alphaVault: getAddress2(
151
170
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
152
171
  ),
153
172
  wrappedTao: getAddress2(
154
173
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
155
174
  ),
175
+ wrappedSn80: getAddress2(
176
+ "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
177
+ ),
156
178
  ccipRouter: getAddress2(
157
179
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
158
180
  ),
@@ -333,23 +355,39 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
333
355
  "function ROUTER() view returns (address)",
334
356
  "function BITTENSOR_SELECTOR() view returns (uint64)",
335
357
  "function SUBTENSOR_GATEWAY() view returns (address)",
358
+ "function maxIntegratorFeeBps() view returns (uint16)",
359
+ "function bridgeFeeBps() view returns (uint16)",
336
360
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
337
361
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
362
+ "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
+ "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)",
338
364
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
339
365
  ]);
340
366
  var ALPHA_GATEWAY_ABI = Object.freeze([
341
367
  "function ROUTER() view returns (address)",
342
368
  "function allowedLane(uint64 selector) view returns (bool)",
369
+ "function maxIntegratorFeeBps() view returns (uint16)",
370
+ "function bridgeFeeBps() view returns (uint16)",
371
+ "function integratorTaoTopUp(uint256 taoAmount,uint16 bps) pure returns (uint256)",
343
372
  "function quoteBridgeOut(uint64 destSelector,address token,address recipient,uint256 mintedAmount) view returns (uint256 fee)",
344
373
  "function bridgeOut(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut) payable returns (bytes32 messageId)",
374
+ "function integratorCut(uint256 amount,uint16 bps) pure returns (uint256)",
375
+ "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)",
376
+ "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
377
+ "function bridgeOutFromValidators(uint64 destSelector,address token,address recipient,uint256 taoAmount,(bytes32 validator,uint256 alphaRao)[] sources,uint256 minTokenOut) payable returns (bytes32 messageId)",
378
+ "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
+ "function MAX_STAKE_SOURCES() view returns (uint256)",
381
+ "function GATEWAY_COLDKEY() view returns (bytes32)",
345
382
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
346
383
  "function claimNative(address to)",
347
384
  "function claimStaked(address token,bytes32 destColdkey,address to)",
348
385
  "function claimToken(address token,address to)",
349
386
  "function claimableNative(address account) view returns (uint256)",
350
- "function claimableToken(address account,address token) view returns (uint256)",
387
+ "function claimableToken(address token,address account) view returns (uint256)",
351
388
  "event BridgedOut(uint64 destChainSelector,address indexed token,address indexed sender,address indexed recipient,uint256 minted,bytes32 messageId)",
352
389
  "event Claimable(address indexed token,address indexed user,uint256 native,uint256 wsn)",
390
+ "event NotDelivered(uint64 indexed sourceChainSelector,address indexed token,uint256 amount,uint8 reason)",
353
391
  "event DeliveredLiquid(address indexed token,bytes32 indexed ss58,uint256 taoOut)",
354
392
  "event DeliveredStaked(address indexed token,bytes32 indexed ss58,uint256 alphaRao)"
355
393
  ]);
@@ -507,6 +545,112 @@ var alphaAbi = parseAbi(ALPHA_GATEWAY_ABI);
507
545
  var stakingAbi = parseAbi(STAKING_ABI);
508
546
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
509
547
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
548
+ var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
549
+ var MAX_PARTNER_FEE_BPS = 1e4;
550
+ var MAX_STAKE_PULLS = 16;
551
+ var NO_PARTNER_FEE = {
552
+ recipient: "0x0000000000000000000000000000000000000000",
553
+ bps: 0
554
+ };
555
+ function resolvePartnerFee(fee, gateway) {
556
+ if (fee === void 0) return NO_PARTNER_FEE;
557
+ if (typeof fee.bps !== "number" || !Number.isInteger(fee.bps) || fee.bps < 0 || fee.bps > MAX_PARTNER_FEE_BPS) {
558
+ throw new ForeverMoneyError(
559
+ "INVALID_PARTNER_FEE",
560
+ `Partner fee bps must be an integer between 0 and ${MAX_PARTNER_FEE_BPS}.`,
561
+ { bps: fee.bps }
562
+ );
563
+ }
564
+ if (fee.bps === 0) return NO_PARTNER_FEE;
565
+ const recipient = normalizeEvmAddress(fee.recipient);
566
+ if (recipient === NO_PARTNER_FEE.recipient || recipient.toLowerCase() === gateway.toLowerCase()) {
567
+ throw new ForeverMoneyError(
568
+ "INVALID_PARTNER_FEE",
569
+ "Partner fee recipient must not be the zero address or the gateway.",
570
+ { recipient }
571
+ );
572
+ }
573
+ return { recipient, bps: fee.bps };
574
+ }
575
+ function partnerFeeCut(amount, bps) {
576
+ return amount * BigInt(bps) / 10000n;
577
+ }
578
+ function partnerFeeTaoTopUp(taoAmount, bps) {
579
+ const raw = partnerFeeCut(taoAmount, bps);
580
+ if (raw === 0n) return 0n;
581
+ return (raw + EVM_WEI_PER_RAO - 1n) / EVM_WEI_PER_RAO * EVM_WEI_PER_RAO;
582
+ }
583
+ function assertPartnerFeeAllowed(fee, maxBps) {
584
+ if (fee.bps > maxBps) {
585
+ throw new ForeverMoneyError(
586
+ "INVALID_PARTNER_FEE",
587
+ `Partner fee of ${fee.bps} bps exceeds the gateway maximum of ${maxBps} bps.`,
588
+ { bps: fee.bps, maxBps }
589
+ );
590
+ }
591
+ }
592
+ function bridgeAsset(evmChain, asset = "tao") {
593
+ if (asset !== "tao" && asset !== "sn80") {
594
+ throw new ForeverMoneyError(
595
+ "INVALID_TRANSACTION_PLAN",
596
+ 'Asset must be "tao" or "sn80".'
597
+ );
598
+ }
599
+ if (asset === "sn80" && evmChain !== "base") {
600
+ throw new ForeverMoneyError(
601
+ "INVALID_TRANSACTION_PLAN",
602
+ "SN80 bridging is supported between Base and Subtensor."
603
+ );
604
+ }
605
+ const evm = getForeverMoneyEvmDeployment(evmChain);
606
+ return asset === "sn80" ? {
607
+ evmToken: foreverMoneyDeployment.base.contracts.wrappedSn80,
608
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedSn80,
609
+ label: "SN80",
610
+ wrappedLabel: "SN80"
611
+ } : {
612
+ evmToken: evm.contracts.wrappedTao,
613
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedTao,
614
+ label: "TAO",
615
+ wrappedLabel: "wrapped TAO"
616
+ };
617
+ }
618
+ function assertAssetMode(asset, mode) {
619
+ if (asset === "sn80" && mode !== "staked") {
620
+ throw new ForeverMoneyError(
621
+ "INVALID_TRANSACTION_PLAN",
622
+ "SN80 bridging requires staked subnet 80 input or delivery; liquid TAO conversion is not supported."
623
+ );
624
+ }
625
+ }
626
+ function sourceNetuid(input) {
627
+ if (input.source !== "staked") return void 0;
628
+ const netuid = input.netuid ?? (input.asset === "sn80" ? SN80_NETUID : void 0);
629
+ if (netuid === void 0) {
630
+ throw new ForeverMoneyError(
631
+ "INVALID_TRANSACTION_PLAN",
632
+ "A netuid is required when bridging staked TAO."
633
+ );
634
+ }
635
+ assertNonNegativeAmount(netuid, "netuid");
636
+ if (input.asset === "sn80" && netuid !== SN80_NETUID) {
637
+ throw new ForeverMoneyError(
638
+ "INVALID_TRANSACTION_PLAN",
639
+ "SN80 staking approvals must use netuid 80."
640
+ );
641
+ }
642
+ return netuid;
643
+ }
644
+ function stakePullsFor(input, amountRao) {
645
+ if (input.stakePulls === void 0) return void 0;
646
+ if (input.source !== "staked") {
647
+ throw new ForeverMoneyError(
648
+ "INVALID_TRANSACTION_PLAN",
649
+ "Stake pulls are only valid for a staked source."
650
+ );
651
+ }
652
+ return resolveStakePulls(input.stakePulls, amountRao);
653
+ }
510
654
  function assertDelivery(value) {
511
655
  if (value !== "liquid" && value !== "staked") {
512
656
  throw new ForeverMoneyError(
@@ -536,6 +680,19 @@ function assertBaseToSubtensorAmount(amountWei, delivery) {
536
680
  );
537
681
  }
538
682
  }
683
+ function assertSubtensorToEvmAmount(amountWei, source) {
684
+ assertWholeRao(amountWei);
685
+ if (source === "liquid" && amountWei < MIN_LIQUID_SUBTENSOR_TO_EVM_WEI) {
686
+ throw new ForeverMoneyError(
687
+ "AMOUNT_BELOW_MINIMUM",
688
+ "Bridging liquid TAO from Subtensor requires at least 0.002 TAO.",
689
+ {
690
+ amountWei: amountWei.toString(),
691
+ minimumAmountWei: MIN_LIQUID_SUBTENSOR_TO_EVM_WEI.toString()
692
+ }
693
+ );
694
+ }
695
+ }
539
696
  function transactionStep(kind, label, chainId, from, to, data, value, gasLimit) {
540
697
  return {
541
698
  kind,
@@ -550,9 +707,159 @@ function transactionStep(kind, label, chainId, from, to, data, value, gasLimit)
550
707
  }
551
708
  };
552
709
  }
710
+ function partnerFeeSummary(charged, unit, fee) {
711
+ if (fee.bps === 0) return "";
712
+ return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
713
+ }
714
+ function encodeSpokeBridge(token, amountWei, exit, fee) {
715
+ return fee.bps === 0 ? encodeFunctionData({
716
+ abi: spokeAbi,
717
+ functionName: "bridgeToFinney",
718
+ args: [token, amountWei, exit]
719
+ }) : encodeFunctionData({
720
+ abi: spokeAbi,
721
+ functionName: "bridgeToFinneyWithFee",
722
+ args: [token, amountWei, exit, 0n, fee]
723
+ });
724
+ }
725
+ function resolveStakePulls(pulls, amountRao) {
726
+ if (pulls.length === 0 || pulls.length > MAX_STAKE_PULLS) {
727
+ throw new ForeverMoneyError(
728
+ "INVALID_TRANSACTION_PLAN",
729
+ `Between 1 and ${MAX_STAKE_PULLS} stake pulls are required.`,
730
+ { count: pulls.length }
731
+ );
732
+ }
733
+ const seen = /* @__PURE__ */ new Set();
734
+ let total = 0n;
735
+ const resolved = pulls.map((pull) => {
736
+ const validator = normalizeBytes32(pull.hotkey, "Stake pull hotkey");
737
+ if (validator === `0x${"0".repeat(64)}`) {
738
+ throw new ForeverMoneyError(
739
+ "INVALID_TRANSACTION_PLAN",
740
+ "Stake pull hotkey must not be zero."
741
+ );
742
+ }
743
+ const key = validator.toLowerCase();
744
+ if (seen.has(key)) {
745
+ throw new ForeverMoneyError(
746
+ "INVALID_TRANSACTION_PLAN",
747
+ "Stake pulls must not repeat a hotkey.",
748
+ { hotkey: validator }
749
+ );
750
+ }
751
+ seen.add(key);
752
+ if (typeof pull.amountRao !== "bigint" || pull.amountRao <= 0n) {
753
+ throw new ForeverMoneyError(
754
+ "INVALID_TRANSACTION_PLAN",
755
+ "Each stake pull must take a positive amount of RAO.",
756
+ { hotkey: validator }
757
+ );
758
+ }
759
+ total += pull.amountRao;
760
+ return { validator, alphaRao: pull.amountRao };
761
+ });
762
+ if (total !== amountRao) {
763
+ throw new ForeverMoneyError(
764
+ "INVALID_TRANSACTION_PLAN",
765
+ "Stake pulls must sum to the bridged amount.",
766
+ { pulledRao: total.toString(), amountRao: amountRao.toString() }
767
+ );
768
+ }
769
+ return resolved;
770
+ }
771
+ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee, pulls) {
772
+ if (pulls !== void 0) {
773
+ const args2 = [
774
+ destSelector,
775
+ token,
776
+ recipient,
777
+ taoAmount,
778
+ pulls,
779
+ minTokenOut
780
+ ];
781
+ return fee.bps === 0 ? encodeFunctionData({
782
+ abi: alphaAbi,
783
+ functionName: "bridgeOutFromValidators",
784
+ args: args2
785
+ }) : encodeFunctionData({
786
+ abi: alphaAbi,
787
+ functionName: "bridgeOutFromValidatorsWithFee",
788
+ args: [...args2, fee]
789
+ });
790
+ }
791
+ const args = [
792
+ destSelector,
793
+ token,
794
+ recipient,
795
+ taoAmount,
796
+ stakedAlphaRao,
797
+ minTokenOut
798
+ ];
799
+ return fee.bps === 0 ? encodeFunctionData({
800
+ abi: alphaAbi,
801
+ functionName: "bridgeOut",
802
+ args
803
+ }) : encodeFunctionData({
804
+ abi: alphaAbi,
805
+ functionName: "bridgeOutWithFee",
806
+ args: [...args, fee]
807
+ });
808
+ }
809
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
810
+ const maxBps = await provider.readContract({
811
+ address: gateway,
812
+ abi: spokeAbi,
813
+ functionName: "maxIntegratorFeeBps"
814
+ });
815
+ assertPartnerFeeAllowed(fee, maxBps);
816
+ const [networkFee] = await provider.readContract({
817
+ address: gateway,
818
+ abi: spokeAbi,
819
+ functionName: "quoteBridgeToFinneyWithFee",
820
+ args: [token, amountWei, exit, 0n, fee]
821
+ });
822
+ return networkFee;
823
+ }
824
+ async function quoteHubWithFee(provider, gateway, destSelector, token, recipient, mintedAmount, taoAmount, stakedAlphaRao, fee) {
825
+ const maxBps = await provider.readContract({
826
+ address: gateway,
827
+ abi: alphaAbi,
828
+ functionName: "maxIntegratorFeeBps"
829
+ });
830
+ assertPartnerFeeAllowed(fee, maxBps);
831
+ const [networkFee, taoTopUp, alphaTopUpRao, crossing] = await provider.readContract({
832
+ address: gateway,
833
+ abi: alphaAbi,
834
+ functionName: "quoteBridgeOutWithFee",
835
+ args: [
836
+ destSelector,
837
+ token,
838
+ recipient,
839
+ mintedAmount,
840
+ taoAmount,
841
+ stakedAlphaRao,
842
+ fee
843
+ ]
844
+ });
845
+ if (crossing !== mintedAmount || taoTopUp !== partnerFeeTaoTopUp(taoAmount, fee.bps) || alphaTopUpRao !== partnerFeeCut(stakedAlphaRao, fee.bps)) {
846
+ throw new ForeverMoneyError(
847
+ "INVALID_PROVIDER_RESPONSE",
848
+ "The gateway partner fee quote does not match the SDK fee calculation.",
849
+ {
850
+ crossing: crossing.toString(),
851
+ taoTopUp: taoTopUp.toString(),
852
+ alphaTopUpRao: alphaTopUpRao.toString()
853
+ }
854
+ );
855
+ }
856
+ return { networkFee, taoTopUp, alphaTopUpRao };
857
+ }
553
858
  function buildEvmToSubtensorPlan(input) {
554
859
  const sender = normalizeEvmAddress(input.sender);
860
+ const asset = bridgeAsset(input.evmChain, input.asset);
555
861
  assertDelivery(input.delivery);
862
+ assertAssetMode(input.asset, input.delivery);
556
863
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
557
864
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
558
865
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
@@ -564,19 +871,24 @@ function buildEvmToSubtensorPlan(input) {
564
871
  minTaoOut: input.amountWei
565
872
  };
566
873
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
874
+ const partnerFee = resolvePartnerFee(
875
+ input.partnerFee,
876
+ evm.contracts.gateway
877
+ );
878
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
567
879
  const steps = [];
568
- if (input.allowanceWei < input.amountWei) {
880
+ if (input.allowanceWei < input.amountWei + cut) {
569
881
  steps.push(
570
882
  transactionStep(
571
883
  "approval",
572
- "Approve wrapped TAO for the ForeverMoney gateway",
884
+ `Approve ${asset.wrappedLabel} for the ForeverMoney gateway`,
573
885
  evm.chainId,
574
886
  sender,
575
- evm.contracts.wrappedTao,
887
+ asset.evmToken,
576
888
  encodeFunctionData({
577
889
  abi: erc20Abi,
578
890
  functionName: "approve",
579
- args: [evm.contracts.gateway, input.amountWei]
891
+ args: [evm.contracts.gateway, input.amountWei + cut]
580
892
  }),
581
893
  0n
582
894
  )
@@ -586,22 +898,23 @@ function buildEvmToSubtensorPlan(input) {
586
898
  steps.push(
587
899
  transactionStep(
588
900
  "transaction",
589
- `Bridge wrapped TAO from ${evm.name} to Subtensor (${input.delivery})`,
901
+ `Bridge ${asset.wrappedLabel} from ${evm.name} to Subtensor (${input.delivery})`,
590
902
  evm.chainId,
591
903
  sender,
592
904
  evm.contracts.gateway,
593
- encodeFunctionData({
594
- abi: spokeAbi,
595
- functionName: "bridgeToFinney",
596
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
597
- }),
905
+ encodeSpokeBridge(
906
+ asset.evmToken,
907
+ input.amountWei,
908
+ exit,
909
+ partnerFee
910
+ ),
598
911
  value,
599
912
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
600
913
  )
601
914
  );
602
915
  return createTransactionPlan({
603
916
  action: evm.key === "base" ? "bridge.base-to-subtensor" : "bridge.robinhood-to-subtensor",
604
- summary: `Bridge ${input.amountWei} wei of wrapped TAO from ${evm.name} to ${destination}.`,
917
+ summary: `Bridge ${input.amountWei} wei of ${asset.wrappedLabel} from ${evm.name} to ${destination}.${partnerFeeSummary(cut, asset.wrappedLabel, partnerFee)}`,
605
918
  steps
606
919
  });
607
920
  }
@@ -611,21 +924,23 @@ function buildBaseToSubtensorPlan(input) {
611
924
  function buildSubtensorToEvmPlan(input) {
612
925
  const sender = normalizeEvmAddress(input.sender);
613
926
  const recipient = normalizeEvmAddress(input.recipient);
927
+ const asset = bridgeAsset(input.evmChain, input.asset);
614
928
  assertSource(input.source);
615
- assertWholeRao(input.amountWei);
929
+ assertAssetMode(input.asset, input.source);
930
+ const netuid = sourceNetuid(input);
931
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
616
932
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
617
933
  const { subtensor } = foreverMoneyDeployment;
618
934
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
935
+ const partnerFee = resolvePartnerFee(
936
+ input.partnerFee,
937
+ subtensor.contracts.gateway
938
+ );
619
939
  const amountRao = input.amountWei / EVM_WEI_PER_RAO;
940
+ const alphaTopUpRao = input.source === "staked" ? partnerFeeCut(amountRao, partnerFee.bps) : 0n;
941
+ const taoTopUp = input.source === "liquid" ? partnerFeeTaoTopUp(input.amountWei, partnerFee.bps) : 0n;
620
942
  const steps = [];
621
943
  if (input.source === "staked") {
622
- if (input.netuid === void 0) {
623
- throw new ForeverMoneyError(
624
- "INVALID_TRANSACTION_PLAN",
625
- "A netuid is required when bridging staked TAO."
626
- );
627
- }
628
- assertNonNegativeAmount(input.netuid, "netuid");
629
944
  if (input.stakingAllowanceRao === void 0) {
630
945
  throw new ForeverMoneyError(
631
946
  "INVALID_TRANSACTION_PLAN",
@@ -633,11 +948,11 @@ function buildSubtensorToEvmPlan(input) {
633
948
  );
634
949
  }
635
950
  assertNonNegativeAmount(input.stakingAllowanceRao, "Staking allowance");
636
- if (input.stakingAllowanceRao < amountRao) {
951
+ if (input.stakingAllowanceRao < amountRao + alphaTopUpRao) {
637
952
  steps.push(
638
953
  transactionStep(
639
954
  "approval",
640
- "Approve staked TAO for the ForeverMoney gateway",
955
+ `Approve staked ${asset.label} for the ForeverMoney gateway`,
641
956
  subtensor.chainId,
642
957
  sender,
643
958
  subtensor.contracts.stakingPrecompile,
@@ -646,8 +961,8 @@ function buildSubtensorToEvmPlan(input) {
646
961
  functionName: "approve",
647
962
  args: [
648
963
  subtensor.contracts.gateway,
649
- input.netuid,
650
- amountRao
964
+ netuid,
965
+ amountRao + alphaTopUpRao
651
966
  ]
652
967
  }),
653
968
  0n
@@ -662,34 +977,37 @@ function buildSubtensorToEvmPlan(input) {
662
977
  }
663
978
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
664
979
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
665
- const value = taoAmount + feeWithBuffer(input.exactNetworkFeeWei);
980
+ const pulls = stakePullsFor(input, amountRao);
981
+ const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
666
982
  assertNonNegativeAmount(value, "Transaction value");
667
983
  steps.push(
668
984
  transactionStep(
669
985
  "transaction",
670
- `Bridge ${input.source} TAO from Subtensor to ${evm.name}`,
986
+ `Bridge ${input.source} ${asset.label} from Subtensor to ${evm.name}`,
671
987
  subtensor.chainId,
672
988
  sender,
673
989
  subtensor.contracts.gateway,
674
- encodeFunctionData({
675
- abi: alphaAbi,
676
- functionName: "bridgeOut",
677
- args: [
678
- evm.ccipSelector,
679
- subtensor.contracts.wrappedTao,
680
- recipient,
681
- taoAmount,
682
- stakedAlphaRao,
683
- input.amountWei
684
- ]
685
- }),
990
+ encodeHubBridge(
991
+ evm.ccipSelector,
992
+ asset.subtensorToken,
993
+ recipient,
994
+ taoAmount,
995
+ stakedAlphaRao,
996
+ input.amountWei,
997
+ partnerFee,
998
+ pulls
999
+ ),
686
1000
  value,
687
1001
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
688
1002
  )
689
1003
  );
690
1004
  return createTransactionPlan({
691
1005
  action: evm.key === "base" ? "bridge.subtensor-to-base" : "bridge.subtensor-to-robinhood",
692
- summary: `Bridge ${input.amountWei} wei of ${input.source} TAO from Subtensor to ${recipient} on ${evm.name}.`,
1006
+ summary: `Bridge ${input.amountWei} wei of ${input.source} ${asset.label} from Subtensor to ${recipient} on ${evm.name}.${partnerFeeSummary(
1007
+ input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
1008
+ input.source === "liquid" ? "TAO" : `staked ${asset.label}`,
1009
+ partnerFee
1010
+ )}`,
693
1011
  steps
694
1012
  });
695
1013
  }
@@ -698,7 +1016,9 @@ function buildSubtensorToBasePlan(input) {
698
1016
  }
699
1017
  async function prepareEvmToSubtensor(provider, input) {
700
1018
  const sender = normalizeEvmAddress(input.sender);
1019
+ const asset = bridgeAsset(input.evmChain, input.asset);
701
1020
  assertDelivery(input.delivery);
1021
+ assertAssetMode(input.asset, input.delivery);
702
1022
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
703
1023
  const destination = normalizeSS58(input.destination);
704
1024
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -708,27 +1028,40 @@ async function prepareEvmToSubtensor(provider, input) {
708
1028
  wantLiquid: input.delivery === "liquid",
709
1029
  minTaoOut: input.amountWei
710
1030
  };
1031
+ const partnerFee = resolvePartnerFee(
1032
+ input.partnerFee,
1033
+ evm.contracts.gateway
1034
+ );
1035
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
711
1036
  const [allowanceWei, exactNetworkFeeWei] = await Promise.all([
712
1037
  provider.readContract({
713
- address: evm.contracts.wrappedTao,
1038
+ address: asset.evmToken,
714
1039
  abi: erc20Abi,
715
1040
  functionName: "allowance",
716
1041
  args: [sender, evm.contracts.gateway]
717
1042
  }),
718
- provider.readContract({
1043
+ partnerFee.bps === 0 ? provider.readContract({
719
1044
  address: evm.contracts.gateway,
720
1045
  abi: spokeAbi,
721
1046
  functionName: "quoteBridgeToFinney",
722
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
723
- })
1047
+ args: [asset.evmToken, input.amountWei, exit]
1048
+ }) : quoteSpokeWithFee(
1049
+ provider,
1050
+ evm.contracts.gateway,
1051
+ asset.evmToken,
1052
+ input.amountWei,
1053
+ exit,
1054
+ partnerFee
1055
+ )
724
1056
  ]);
725
1057
  let estimatedBridgeGas;
726
- if (allowanceWei >= input.amountWei) {
727
- const data = encodeFunctionData({
728
- abi: spokeAbi,
729
- functionName: "bridgeToFinney",
730
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
731
- });
1058
+ if (allowanceWei >= input.amountWei + cut) {
1059
+ const data = encodeSpokeBridge(
1060
+ asset.evmToken,
1061
+ input.amountWei,
1062
+ exit,
1063
+ partnerFee
1064
+ );
732
1065
  estimatedBridgeGas = await provider.estimateGas({
733
1066
  account: sender,
734
1067
  to: evm.contracts.gateway,
@@ -745,6 +1078,7 @@ async function prepareEvmToSubtensor(provider, input) {
745
1078
  return Object.freeze({
746
1079
  exactNetworkFeeWei,
747
1080
  transactionValueWei: feeWithBuffer(exactNetworkFeeWei),
1081
+ partnerFeeWei: cut,
748
1082
  plan
749
1083
  });
750
1084
  }
@@ -754,58 +1088,68 @@ function prepareBaseToSubtensor(provider, input) {
754
1088
  async function prepareSubtensorToEvm(provider, input) {
755
1089
  const sender = normalizeEvmAddress(input.sender);
756
1090
  const recipient = normalizeEvmAddress(input.recipient);
1091
+ const asset = bridgeAsset(input.evmChain, input.asset);
757
1092
  assertSource(input.source);
758
- assertWholeRao(input.amountWei);
1093
+ assertAssetMode(input.asset, input.source);
1094
+ const netuid = sourceNetuid(input);
1095
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
759
1096
  const { subtensor } = foreverMoneyDeployment;
760
1097
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
761
- const exactNetworkFeeWei = await provider.readContract({
1098
+ const partnerFee = resolvePartnerFee(
1099
+ input.partnerFee,
1100
+ subtensor.contracts.gateway
1101
+ );
1102
+ const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
1103
+ const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
1104
+ const exactNetworkFeeWei = partnerFee.bps === 0 ? await provider.readContract({
762
1105
  address: subtensor.contracts.gateway,
763
1106
  abi: alphaAbi,
764
1107
  functionName: "quoteBridgeOut",
765
1108
  args: [
766
1109
  evm.ccipSelector,
767
- subtensor.contracts.wrappedTao,
1110
+ asset.subtensorToken,
768
1111
  recipient,
769
1112
  input.amountWei
770
1113
  ]
771
- });
1114
+ }) : (await quoteHubWithFee(
1115
+ provider,
1116
+ subtensor.contracts.gateway,
1117
+ evm.ccipSelector,
1118
+ asset.subtensorToken,
1119
+ recipient,
1120
+ input.amountWei,
1121
+ taoAmount,
1122
+ stakedAlphaRao,
1123
+ partnerFee
1124
+ )).networkFee;
772
1125
  let stakingAllowanceRao;
773
1126
  if (input.source === "staked") {
774
- if (input.netuid === void 0) {
775
- throw new ForeverMoneyError(
776
- "INVALID_TRANSACTION_PLAN",
777
- "A netuid is required when bridging staked TAO."
778
- );
779
- }
780
- assertNonNegativeAmount(input.netuid, "netuid");
781
1127
  stakingAllowanceRao = await provider.readContract({
782
1128
  address: subtensor.contracts.stakingPrecompile,
783
1129
  abi: stakingAbi,
784
1130
  functionName: "allowance",
785
- args: [sender, subtensor.contracts.gateway, input.netuid]
1131
+ args: [sender, subtensor.contracts.gateway, netuid]
786
1132
  });
787
1133
  }
788
- const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
789
- const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
790
- const value = taoAmount + feeWithBuffer(exactNetworkFeeWei);
1134
+ const alphaTopUpRao = partnerFeeCut(stakedAlphaRao, partnerFee.bps);
1135
+ const taoTopUp = partnerFeeTaoTopUp(taoAmount, partnerFee.bps);
1136
+ const value = taoAmount + taoTopUp + feeWithBuffer(exactNetworkFeeWei);
791
1137
  assertNonNegativeAmount(value, "Transaction value");
792
1138
  let estimatedBridgeGas;
793
- if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao) {
1139
+ if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao + alphaTopUpRao) {
794
1140
  estimatedBridgeGas = await provider.estimateGas({
795
1141
  account: sender,
796
1142
  to: subtensor.contracts.gateway,
797
- data: encodeFunctionData({
798
- abi: alphaAbi,
799
- functionName: "bridgeOut",
800
- args: [
801
- evm.ccipSelector,
802
- subtensor.contracts.wrappedTao,
803
- recipient,
804
- taoAmount,
805
- stakedAlphaRao,
806
- input.amountWei
807
- ]
808
- }),
1143
+ data: encodeHubBridge(
1144
+ evm.ccipSelector,
1145
+ asset.subtensorToken,
1146
+ recipient,
1147
+ taoAmount,
1148
+ stakedAlphaRao,
1149
+ input.amountWei,
1150
+ partnerFee,
1151
+ stakePullsFor(input, stakedAlphaRao)
1152
+ ),
809
1153
  value
810
1154
  });
811
1155
  }
@@ -818,6 +1162,7 @@ async function prepareSubtensorToEvm(provider, input) {
818
1162
  return Object.freeze({
819
1163
  exactNetworkFeeWei,
820
1164
  transactionValueWei: value,
1165
+ partnerFeeWei: input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
821
1166
  plan
822
1167
  });
823
1168
  }
@@ -1417,13 +1762,20 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1417
1762
  evmChainFromBridgeDirection(direction)
1418
1763
  );
1419
1764
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1420
- const [address, contractAbi, eventName] = evmToSubtensor ? [evm.contracts.gateway, spokeGatewayAbi, "BridgedToFinney"] : [
1421
- foreverMoneyDeployment.subtensor.contracts.gateway,
1765
+ const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1766
+ [...evm.contracts.legacyGateways, evm.contracts.gateway],
1767
+ spokeGatewayAbi,
1768
+ "BridgedToFinney"
1769
+ ] : [
1770
+ [
1771
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1772
+ foreverMoneyDeployment.subtensor.contracts.gateway
1773
+ ],
1422
1774
  alphaGatewayAbi,
1423
1775
  "BridgedOut"
1424
1776
  ];
1425
1777
  for (const log of receipt.logs) {
1426
- if (!isLogFrom(log, address)) continue;
1778
+ if (!addresses.some((address) => isLogFrom(log, address))) continue;
1427
1779
  try {
1428
1780
  const parsed = decodeEventLog({
1429
1781
  abi: contractAbi,
@@ -1577,16 +1929,23 @@ async function getCcipDeliveryStatus(provider, input) {
1577
1929
  "The CCIP execution receipt is unavailable."
1578
1930
  );
1579
1931
  }
1932
+ const subtensorGateways = [
1933
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1934
+ foreverMoneyDeployment.subtensor.contracts.gateway
1935
+ ];
1580
1936
  for (const log of receipt.logs) {
1581
- if (log.address.toLowerCase() !== foreverMoneyDeployment.subtensor.contracts.gateway.toLowerCase()) {
1937
+ if (!subtensorGateways.some(
1938
+ (gateway) => log.address.toLowerCase() === gateway.toLowerCase()
1939
+ )) {
1582
1940
  continue;
1583
1941
  }
1584
1942
  try {
1585
- if (decodeEventLog2({
1943
+ const { eventName } = decodeEventLog2({
1586
1944
  abi: alphaGatewayAbi2,
1587
1945
  data: log.data,
1588
1946
  topics: log.topics
1589
- }).eventName === "Claimable") {
1947
+ });
1948
+ if (eventName === "Claimable" || eventName === "NotDelivered") {
1590
1949
  return "recovery";
1591
1950
  }
1592
1951
  } catch {
@@ -1872,12 +2231,16 @@ export {
1872
2231
  FOREVERMONEY_DEPLOYMENT_VERSION,
1873
2232
  ForeverMoneyError,
1874
2233
  GAS_LIMIT_BUFFER_BPS,
2234
+ MAX_PARTNER_FEE_BPS,
2235
+ MAX_STAKE_PULLS,
1875
2236
  MIN_LIQUID_BASE_TO_SUBTENSOR_WEI,
1876
2237
  MIN_LIQUID_EVM_TO_SUBTENSOR_WEI,
2238
+ MIN_LIQUID_SUBTENSOR_TO_EVM_WEI,
1877
2239
  NETWORK_FEE_BUFFER_BPS,
1878
2240
  RAO_PER_TAO,
1879
2241
  ROBINHOOD_CCIP_SELECTOR,
1880
2242
  ROBINHOOD_CHAIN_ID,
2243
+ SN80_NETUID,
1881
2244
  SUBTENSOR_CCIP_SELECTOR,
1882
2245
  SUBTENSOR_CHAIN_ID,
1883
2246
  assertWholeRao,
@@ -1910,6 +2273,8 @@ export {
1910
2273
  normalizeEvmAddress,
1911
2274
  normalizeSS58,
1912
2275
  parseTaoAmount,
2276
+ partnerFeeCut,
2277
+ partnerFeeTaoTopUp,
1913
2278
  sourceChainId,
1914
2279
  ss58ToPublicKey,
1915
2280
  toEip1193Transaction,