@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.cjs CHANGED
@@ -27,12 +27,16 @@ __export(index_exports, {
27
27
  FOREVERMONEY_DEPLOYMENT_VERSION: () => FOREVERMONEY_DEPLOYMENT_VERSION,
28
28
  ForeverMoneyError: () => ForeverMoneyError,
29
29
  GAS_LIMIT_BUFFER_BPS: () => GAS_LIMIT_BUFFER_BPS,
30
+ MAX_PARTNER_FEE_BPS: () => MAX_PARTNER_FEE_BPS,
31
+ MAX_STAKE_PULLS: () => MAX_STAKE_PULLS,
30
32
  MIN_LIQUID_BASE_TO_SUBTENSOR_WEI: () => MIN_LIQUID_BASE_TO_SUBTENSOR_WEI,
31
33
  MIN_LIQUID_EVM_TO_SUBTENSOR_WEI: () => MIN_LIQUID_EVM_TO_SUBTENSOR_WEI,
34
+ MIN_LIQUID_SUBTENSOR_TO_EVM_WEI: () => MIN_LIQUID_SUBTENSOR_TO_EVM_WEI,
32
35
  NETWORK_FEE_BUFFER_BPS: () => NETWORK_FEE_BUFFER_BPS,
33
36
  RAO_PER_TAO: () => RAO_PER_TAO,
34
37
  ROBINHOOD_CCIP_SELECTOR: () => ROBINHOOD_CCIP_SELECTOR,
35
38
  ROBINHOOD_CHAIN_ID: () => ROBINHOOD_CHAIN_ID,
39
+ SN80_NETUID: () => SN80_NETUID,
36
40
  SUBTENSOR_CCIP_SELECTOR: () => SUBTENSOR_CCIP_SELECTOR,
37
41
  SUBTENSOR_CHAIN_ID: () => SUBTENSOR_CHAIN_ID,
38
42
  assertWholeRao: () => assertWholeRao,
@@ -65,6 +69,8 @@ __export(index_exports, {
65
69
  normalizeEvmAddress: () => normalizeEvmAddress,
66
70
  normalizeSS58: () => normalizeSS58,
67
71
  parseTaoAmount: () => parseTaoAmount,
72
+ partnerFeeCut: () => partnerFeeCut,
73
+ partnerFeeTaoTopUp: () => partnerFeeTaoTopUp,
68
74
  sourceChainId: () => sourceChainId,
69
75
  ss58ToPublicKey: () => ss58ToPublicKey,
70
76
  toEip1193Transaction: () => toEip1193Transaction,
@@ -156,7 +162,8 @@ var ROBINHOOD_CCIP_SELECTOR = 6180753054346818345n;
156
162
  var SUBTENSOR_CCIP_SELECTOR = 2135107236357186872n;
157
163
  var RAO_PER_TAO = 1000000000n;
158
164
  var EVM_WEI_PER_RAO = 1000000000n;
159
- var FOREVERMONEY_DEPLOYMENT_VERSION = "1.1.0";
165
+ var SN80_NETUID = 80n;
166
+ var FOREVERMONEY_DEPLOYMENT_VERSION = "1.3.0";
160
167
  var foreverMoneyDeployment = Object.freeze({
161
168
  version: FOREVERMONEY_DEPLOYMENT_VERSION,
162
169
  base: Object.freeze({
@@ -165,10 +172,18 @@ var foreverMoneyDeployment = Object.freeze({
165
172
  chainId: BASE_CHAIN_ID,
166
173
  ccipSelector: BASE_CCIP_SELECTOR,
167
174
  contracts: Object.freeze({
168
- gateway: (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"),
175
+ gateway: (0, import_viem2.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
176
+ // Retired gateways, oldest first; kept only to track transfers sent
177
+ // through them.
178
+ legacyGateways: [
179
+ (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C")
180
+ ],
169
181
  wrappedTao: (0, import_viem2.getAddress)(
170
182
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
171
183
  ),
184
+ wrappedSn80: (0, import_viem2.getAddress)(
185
+ "0x6f63d869011f95274498023b4abfc00b30c34378"
186
+ ),
172
187
  ccipRouter: (0, import_viem2.getAddress)(
173
188
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
174
189
  ),
@@ -194,7 +209,10 @@ var foreverMoneyDeployment = Object.freeze({
194
209
  chainId: ROBINHOOD_CHAIN_ID,
195
210
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
196
211
  contracts: Object.freeze({
197
- gateway: (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"),
212
+ gateway: (0, import_viem2.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
213
+ legacyGateways: [
214
+ (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8")
215
+ ],
198
216
  wrappedTao: (0, import_viem2.getAddress)(
199
217
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
200
218
  ),
@@ -210,13 +228,23 @@ var foreverMoneyDeployment = Object.freeze({
210
228
  chainId: SUBTENSOR_CHAIN_ID,
211
229
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
212
230
  contracts: Object.freeze({
213
- gateway: (0, import_viem2.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
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
+ ],
214
239
  alphaVault: (0, import_viem2.getAddress)(
215
240
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
216
241
  ),
217
242
  wrappedTao: (0, import_viem2.getAddress)(
218
243
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
219
244
  ),
245
+ wrappedSn80: (0, import_viem2.getAddress)(
246
+ "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
247
+ ),
220
248
  ccipRouter: (0, import_viem2.getAddress)(
221
249
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
222
250
  ),
@@ -397,23 +425,39 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
397
425
  "function ROUTER() view returns (address)",
398
426
  "function BITTENSOR_SELECTOR() view returns (uint64)",
399
427
  "function SUBTENSOR_GATEWAY() view returns (address)",
428
+ "function maxIntegratorFeeBps() view returns (uint16)",
429
+ "function bridgeFeeBps() view returns (uint16)",
400
430
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
401
431
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
432
+ "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)",
433
+ "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)",
402
434
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
403
435
  ]);
404
436
  var ALPHA_GATEWAY_ABI = Object.freeze([
405
437
  "function ROUTER() view returns (address)",
406
438
  "function allowedLane(uint64 selector) view returns (bool)",
439
+ "function maxIntegratorFeeBps() view returns (uint16)",
440
+ "function bridgeFeeBps() view returns (uint16)",
441
+ "function integratorTaoTopUp(uint256 taoAmount,uint16 bps) pure returns (uint256)",
407
442
  "function quoteBridgeOut(uint64 destSelector,address token,address recipient,uint256 mintedAmount) view returns (uint256 fee)",
408
443
  "function bridgeOut(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut) payable returns (bytes32 messageId)",
444
+ "function integratorCut(uint256 amount,uint16 bps) pure returns (uint256)",
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)",
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)",
409
452
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
410
453
  "function claimNative(address to)",
411
454
  "function claimStaked(address token,bytes32 destColdkey,address to)",
412
455
  "function claimToken(address token,address to)",
413
456
  "function claimableNative(address account) view returns (uint256)",
414
- "function claimableToken(address account,address token) view returns (uint256)",
457
+ "function claimableToken(address token,address account) view returns (uint256)",
415
458
  "event BridgedOut(uint64 destChainSelector,address indexed token,address indexed sender,address indexed recipient,uint256 minted,bytes32 messageId)",
416
459
  "event Claimable(address indexed token,address indexed user,uint256 native,uint256 wsn)",
460
+ "event NotDelivered(uint64 indexed sourceChainSelector,address indexed token,uint256 amount,uint8 reason)",
417
461
  "event DeliveredLiquid(address indexed token,bytes32 indexed ss58,uint256 taoOut)",
418
462
  "event DeliveredStaked(address indexed token,bytes32 indexed ss58,uint256 alphaRao)"
419
463
  ]);
@@ -571,6 +615,112 @@ var alphaAbi = (0, import_viem6.parseAbi)(ALPHA_GATEWAY_ABI);
571
615
  var stakingAbi = (0, import_viem6.parseAbi)(STAKING_ABI);
572
616
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
573
617
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
618
+ var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
619
+ var MAX_PARTNER_FEE_BPS = 1e4;
620
+ var MAX_STAKE_PULLS = 16;
621
+ var NO_PARTNER_FEE = {
622
+ recipient: "0x0000000000000000000000000000000000000000",
623
+ bps: 0
624
+ };
625
+ function resolvePartnerFee(fee, gateway) {
626
+ if (fee === void 0) return NO_PARTNER_FEE;
627
+ if (typeof fee.bps !== "number" || !Number.isInteger(fee.bps) || fee.bps < 0 || fee.bps > MAX_PARTNER_FEE_BPS) {
628
+ throw new ForeverMoneyError(
629
+ "INVALID_PARTNER_FEE",
630
+ `Partner fee bps must be an integer between 0 and ${MAX_PARTNER_FEE_BPS}.`,
631
+ { bps: fee.bps }
632
+ );
633
+ }
634
+ if (fee.bps === 0) return NO_PARTNER_FEE;
635
+ const recipient = normalizeEvmAddress(fee.recipient);
636
+ if (recipient === NO_PARTNER_FEE.recipient || recipient.toLowerCase() === gateway.toLowerCase()) {
637
+ throw new ForeverMoneyError(
638
+ "INVALID_PARTNER_FEE",
639
+ "Partner fee recipient must not be the zero address or the gateway.",
640
+ { recipient }
641
+ );
642
+ }
643
+ return { recipient, bps: fee.bps };
644
+ }
645
+ function partnerFeeCut(amount, bps) {
646
+ return amount * BigInt(bps) / 10000n;
647
+ }
648
+ function partnerFeeTaoTopUp(taoAmount, bps) {
649
+ const raw = partnerFeeCut(taoAmount, bps);
650
+ if (raw === 0n) return 0n;
651
+ return (raw + EVM_WEI_PER_RAO - 1n) / EVM_WEI_PER_RAO * EVM_WEI_PER_RAO;
652
+ }
653
+ function assertPartnerFeeAllowed(fee, maxBps) {
654
+ if (fee.bps > maxBps) {
655
+ throw new ForeverMoneyError(
656
+ "INVALID_PARTNER_FEE",
657
+ `Partner fee of ${fee.bps} bps exceeds the gateway maximum of ${maxBps} bps.`,
658
+ { bps: fee.bps, maxBps }
659
+ );
660
+ }
661
+ }
662
+ function bridgeAsset(evmChain, asset = "tao") {
663
+ if (asset !== "tao" && asset !== "sn80") {
664
+ throw new ForeverMoneyError(
665
+ "INVALID_TRANSACTION_PLAN",
666
+ 'Asset must be "tao" or "sn80".'
667
+ );
668
+ }
669
+ if (asset === "sn80" && evmChain !== "base") {
670
+ throw new ForeverMoneyError(
671
+ "INVALID_TRANSACTION_PLAN",
672
+ "SN80 bridging is supported between Base and Subtensor."
673
+ );
674
+ }
675
+ const evm = getForeverMoneyEvmDeployment(evmChain);
676
+ return asset === "sn80" ? {
677
+ evmToken: foreverMoneyDeployment.base.contracts.wrappedSn80,
678
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedSn80,
679
+ label: "SN80",
680
+ wrappedLabel: "SN80"
681
+ } : {
682
+ evmToken: evm.contracts.wrappedTao,
683
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedTao,
684
+ label: "TAO",
685
+ wrappedLabel: "wrapped TAO"
686
+ };
687
+ }
688
+ function assertAssetMode(asset, mode) {
689
+ if (asset === "sn80" && mode !== "staked") {
690
+ throw new ForeverMoneyError(
691
+ "INVALID_TRANSACTION_PLAN",
692
+ "SN80 bridging requires staked subnet 80 input or delivery; liquid TAO conversion is not supported."
693
+ );
694
+ }
695
+ }
696
+ function sourceNetuid(input) {
697
+ if (input.source !== "staked") return void 0;
698
+ const netuid = input.netuid ?? (input.asset === "sn80" ? SN80_NETUID : void 0);
699
+ if (netuid === void 0) {
700
+ throw new ForeverMoneyError(
701
+ "INVALID_TRANSACTION_PLAN",
702
+ "A netuid is required when bridging staked TAO."
703
+ );
704
+ }
705
+ assertNonNegativeAmount(netuid, "netuid");
706
+ if (input.asset === "sn80" && netuid !== SN80_NETUID) {
707
+ throw new ForeverMoneyError(
708
+ "INVALID_TRANSACTION_PLAN",
709
+ "SN80 staking approvals must use netuid 80."
710
+ );
711
+ }
712
+ return netuid;
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
+ }
574
724
  function assertDelivery(value) {
575
725
  if (value !== "liquid" && value !== "staked") {
576
726
  throw new ForeverMoneyError(
@@ -600,6 +750,19 @@ function assertBaseToSubtensorAmount(amountWei, delivery) {
600
750
  );
601
751
  }
602
752
  }
753
+ function assertSubtensorToEvmAmount(amountWei, source) {
754
+ assertWholeRao(amountWei);
755
+ if (source === "liquid" && amountWei < MIN_LIQUID_SUBTENSOR_TO_EVM_WEI) {
756
+ throw new ForeverMoneyError(
757
+ "AMOUNT_BELOW_MINIMUM",
758
+ "Bridging liquid TAO from Subtensor requires at least 0.002 TAO.",
759
+ {
760
+ amountWei: amountWei.toString(),
761
+ minimumAmountWei: MIN_LIQUID_SUBTENSOR_TO_EVM_WEI.toString()
762
+ }
763
+ );
764
+ }
765
+ }
603
766
  function transactionStep(kind, label, chainId, from, to, data, value, gasLimit) {
604
767
  return {
605
768
  kind,
@@ -614,9 +777,159 @@ function transactionStep(kind, label, chainId, from, to, data, value, gasLimit)
614
777
  }
615
778
  };
616
779
  }
780
+ function partnerFeeSummary(charged, unit, fee) {
781
+ if (fee.bps === 0) return "";
782
+ return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
783
+ }
784
+ function encodeSpokeBridge(token, amountWei, exit, fee) {
785
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
786
+ abi: spokeAbi,
787
+ functionName: "bridgeToFinney",
788
+ args: [token, amountWei, exit]
789
+ }) : (0, import_viem6.encodeFunctionData)({
790
+ abi: spokeAbi,
791
+ functionName: "bridgeToFinneyWithFee",
792
+ args: [token, amountWei, exit, 0n, fee]
793
+ });
794
+ }
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
+ }
861
+ const args = [
862
+ destSelector,
863
+ token,
864
+ recipient,
865
+ taoAmount,
866
+ stakedAlphaRao,
867
+ minTokenOut
868
+ ];
869
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
870
+ abi: alphaAbi,
871
+ functionName: "bridgeOut",
872
+ args
873
+ }) : (0, import_viem6.encodeFunctionData)({
874
+ abi: alphaAbi,
875
+ functionName: "bridgeOutWithFee",
876
+ args: [...args, fee]
877
+ });
878
+ }
879
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
880
+ const maxBps = await provider.readContract({
881
+ address: gateway,
882
+ abi: spokeAbi,
883
+ functionName: "maxIntegratorFeeBps"
884
+ });
885
+ assertPartnerFeeAllowed(fee, maxBps);
886
+ const [networkFee] = await provider.readContract({
887
+ address: gateway,
888
+ abi: spokeAbi,
889
+ functionName: "quoteBridgeToFinneyWithFee",
890
+ args: [token, amountWei, exit, 0n, fee]
891
+ });
892
+ return networkFee;
893
+ }
894
+ async function quoteHubWithFee(provider, gateway, destSelector, token, recipient, mintedAmount, taoAmount, stakedAlphaRao, fee) {
895
+ const maxBps = await provider.readContract({
896
+ address: gateway,
897
+ abi: alphaAbi,
898
+ functionName: "maxIntegratorFeeBps"
899
+ });
900
+ assertPartnerFeeAllowed(fee, maxBps);
901
+ const [networkFee, taoTopUp, alphaTopUpRao, crossing] = await provider.readContract({
902
+ address: gateway,
903
+ abi: alphaAbi,
904
+ functionName: "quoteBridgeOutWithFee",
905
+ args: [
906
+ destSelector,
907
+ token,
908
+ recipient,
909
+ mintedAmount,
910
+ taoAmount,
911
+ stakedAlphaRao,
912
+ fee
913
+ ]
914
+ });
915
+ if (crossing !== mintedAmount || taoTopUp !== partnerFeeTaoTopUp(taoAmount, fee.bps) || alphaTopUpRao !== partnerFeeCut(stakedAlphaRao, fee.bps)) {
916
+ throw new ForeverMoneyError(
917
+ "INVALID_PROVIDER_RESPONSE",
918
+ "The gateway partner fee quote does not match the SDK fee calculation.",
919
+ {
920
+ crossing: crossing.toString(),
921
+ taoTopUp: taoTopUp.toString(),
922
+ alphaTopUpRao: alphaTopUpRao.toString()
923
+ }
924
+ );
925
+ }
926
+ return { networkFee, taoTopUp, alphaTopUpRao };
927
+ }
617
928
  function buildEvmToSubtensorPlan(input) {
618
929
  const sender = normalizeEvmAddress(input.sender);
930
+ const asset = bridgeAsset(input.evmChain, input.asset);
619
931
  assertDelivery(input.delivery);
932
+ assertAssetMode(input.asset, input.delivery);
620
933
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
621
934
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
622
935
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
@@ -628,19 +941,24 @@ function buildEvmToSubtensorPlan(input) {
628
941
  minTaoOut: input.amountWei
629
942
  };
630
943
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
944
+ const partnerFee = resolvePartnerFee(
945
+ input.partnerFee,
946
+ evm.contracts.gateway
947
+ );
948
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
631
949
  const steps = [];
632
- if (input.allowanceWei < input.amountWei) {
950
+ if (input.allowanceWei < input.amountWei + cut) {
633
951
  steps.push(
634
952
  transactionStep(
635
953
  "approval",
636
- "Approve wrapped TAO for the ForeverMoney gateway",
954
+ `Approve ${asset.wrappedLabel} for the ForeverMoney gateway`,
637
955
  evm.chainId,
638
956
  sender,
639
- evm.contracts.wrappedTao,
957
+ asset.evmToken,
640
958
  (0, import_viem6.encodeFunctionData)({
641
959
  abi: erc20Abi,
642
960
  functionName: "approve",
643
- args: [evm.contracts.gateway, input.amountWei]
961
+ args: [evm.contracts.gateway, input.amountWei + cut]
644
962
  }),
645
963
  0n
646
964
  )
@@ -650,22 +968,23 @@ function buildEvmToSubtensorPlan(input) {
650
968
  steps.push(
651
969
  transactionStep(
652
970
  "transaction",
653
- `Bridge wrapped TAO from ${evm.name} to Subtensor (${input.delivery})`,
971
+ `Bridge ${asset.wrappedLabel} from ${evm.name} to Subtensor (${input.delivery})`,
654
972
  evm.chainId,
655
973
  sender,
656
974
  evm.contracts.gateway,
657
- (0, import_viem6.encodeFunctionData)({
658
- abi: spokeAbi,
659
- functionName: "bridgeToFinney",
660
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
661
- }),
975
+ encodeSpokeBridge(
976
+ asset.evmToken,
977
+ input.amountWei,
978
+ exit,
979
+ partnerFee
980
+ ),
662
981
  value,
663
982
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
664
983
  )
665
984
  );
666
985
  return createTransactionPlan({
667
986
  action: evm.key === "base" ? "bridge.base-to-subtensor" : "bridge.robinhood-to-subtensor",
668
- summary: `Bridge ${input.amountWei} wei of wrapped TAO from ${evm.name} to ${destination}.`,
987
+ summary: `Bridge ${input.amountWei} wei of ${asset.wrappedLabel} from ${evm.name} to ${destination}.${partnerFeeSummary(cut, asset.wrappedLabel, partnerFee)}`,
669
988
  steps
670
989
  });
671
990
  }
@@ -675,21 +994,23 @@ function buildBaseToSubtensorPlan(input) {
675
994
  function buildSubtensorToEvmPlan(input) {
676
995
  const sender = normalizeEvmAddress(input.sender);
677
996
  const recipient = normalizeEvmAddress(input.recipient);
997
+ const asset = bridgeAsset(input.evmChain, input.asset);
678
998
  assertSource(input.source);
679
- assertWholeRao(input.amountWei);
999
+ assertAssetMode(input.asset, input.source);
1000
+ const netuid = sourceNetuid(input);
1001
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
680
1002
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
681
1003
  const { subtensor } = foreverMoneyDeployment;
682
1004
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
1005
+ const partnerFee = resolvePartnerFee(
1006
+ input.partnerFee,
1007
+ subtensor.contracts.gateway
1008
+ );
683
1009
  const amountRao = input.amountWei / EVM_WEI_PER_RAO;
1010
+ const alphaTopUpRao = input.source === "staked" ? partnerFeeCut(amountRao, partnerFee.bps) : 0n;
1011
+ const taoTopUp = input.source === "liquid" ? partnerFeeTaoTopUp(input.amountWei, partnerFee.bps) : 0n;
684
1012
  const steps = [];
685
1013
  if (input.source === "staked") {
686
- if (input.netuid === void 0) {
687
- throw new ForeverMoneyError(
688
- "INVALID_TRANSACTION_PLAN",
689
- "A netuid is required when bridging staked TAO."
690
- );
691
- }
692
- assertNonNegativeAmount(input.netuid, "netuid");
693
1014
  if (input.stakingAllowanceRao === void 0) {
694
1015
  throw new ForeverMoneyError(
695
1016
  "INVALID_TRANSACTION_PLAN",
@@ -697,11 +1018,11 @@ function buildSubtensorToEvmPlan(input) {
697
1018
  );
698
1019
  }
699
1020
  assertNonNegativeAmount(input.stakingAllowanceRao, "Staking allowance");
700
- if (input.stakingAllowanceRao < amountRao) {
1021
+ if (input.stakingAllowanceRao < amountRao + alphaTopUpRao) {
701
1022
  steps.push(
702
1023
  transactionStep(
703
1024
  "approval",
704
- "Approve staked TAO for the ForeverMoney gateway",
1025
+ `Approve staked ${asset.label} for the ForeverMoney gateway`,
705
1026
  subtensor.chainId,
706
1027
  sender,
707
1028
  subtensor.contracts.stakingPrecompile,
@@ -710,8 +1031,8 @@ function buildSubtensorToEvmPlan(input) {
710
1031
  functionName: "approve",
711
1032
  args: [
712
1033
  subtensor.contracts.gateway,
713
- input.netuid,
714
- amountRao
1034
+ netuid,
1035
+ amountRao + alphaTopUpRao
715
1036
  ]
716
1037
  }),
717
1038
  0n
@@ -726,34 +1047,37 @@ function buildSubtensorToEvmPlan(input) {
726
1047
  }
727
1048
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
728
1049
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
729
- const value = taoAmount + feeWithBuffer(input.exactNetworkFeeWei);
1050
+ const pulls = stakePullsFor(input, amountRao);
1051
+ const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
730
1052
  assertNonNegativeAmount(value, "Transaction value");
731
1053
  steps.push(
732
1054
  transactionStep(
733
1055
  "transaction",
734
- `Bridge ${input.source} TAO from Subtensor to ${evm.name}`,
1056
+ `Bridge ${input.source} ${asset.label} from Subtensor to ${evm.name}`,
735
1057
  subtensor.chainId,
736
1058
  sender,
737
1059
  subtensor.contracts.gateway,
738
- (0, import_viem6.encodeFunctionData)({
739
- abi: alphaAbi,
740
- functionName: "bridgeOut",
741
- args: [
742
- evm.ccipSelector,
743
- subtensor.contracts.wrappedTao,
744
- recipient,
745
- taoAmount,
746
- stakedAlphaRao,
747
- input.amountWei
748
- ]
749
- }),
1060
+ encodeHubBridge(
1061
+ evm.ccipSelector,
1062
+ asset.subtensorToken,
1063
+ recipient,
1064
+ taoAmount,
1065
+ stakedAlphaRao,
1066
+ input.amountWei,
1067
+ partnerFee,
1068
+ pulls
1069
+ ),
750
1070
  value,
751
1071
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
752
1072
  )
753
1073
  );
754
1074
  return createTransactionPlan({
755
1075
  action: evm.key === "base" ? "bridge.subtensor-to-base" : "bridge.subtensor-to-robinhood",
756
- summary: `Bridge ${input.amountWei} wei of ${input.source} TAO from Subtensor to ${recipient} on ${evm.name}.`,
1076
+ summary: `Bridge ${input.amountWei} wei of ${input.source} ${asset.label} from Subtensor to ${recipient} on ${evm.name}.${partnerFeeSummary(
1077
+ input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
1078
+ input.source === "liquid" ? "TAO" : `staked ${asset.label}`,
1079
+ partnerFee
1080
+ )}`,
757
1081
  steps
758
1082
  });
759
1083
  }
@@ -762,7 +1086,9 @@ function buildSubtensorToBasePlan(input) {
762
1086
  }
763
1087
  async function prepareEvmToSubtensor(provider, input) {
764
1088
  const sender = normalizeEvmAddress(input.sender);
1089
+ const asset = bridgeAsset(input.evmChain, input.asset);
765
1090
  assertDelivery(input.delivery);
1091
+ assertAssetMode(input.asset, input.delivery);
766
1092
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
767
1093
  const destination = normalizeSS58(input.destination);
768
1094
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -772,27 +1098,40 @@ async function prepareEvmToSubtensor(provider, input) {
772
1098
  wantLiquid: input.delivery === "liquid",
773
1099
  minTaoOut: input.amountWei
774
1100
  };
1101
+ const partnerFee = resolvePartnerFee(
1102
+ input.partnerFee,
1103
+ evm.contracts.gateway
1104
+ );
1105
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
775
1106
  const [allowanceWei, exactNetworkFeeWei] = await Promise.all([
776
1107
  provider.readContract({
777
- address: evm.contracts.wrappedTao,
1108
+ address: asset.evmToken,
778
1109
  abi: erc20Abi,
779
1110
  functionName: "allowance",
780
1111
  args: [sender, evm.contracts.gateway]
781
1112
  }),
782
- provider.readContract({
1113
+ partnerFee.bps === 0 ? provider.readContract({
783
1114
  address: evm.contracts.gateway,
784
1115
  abi: spokeAbi,
785
1116
  functionName: "quoteBridgeToFinney",
786
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
787
- })
1117
+ args: [asset.evmToken, input.amountWei, exit]
1118
+ }) : quoteSpokeWithFee(
1119
+ provider,
1120
+ evm.contracts.gateway,
1121
+ asset.evmToken,
1122
+ input.amountWei,
1123
+ exit,
1124
+ partnerFee
1125
+ )
788
1126
  ]);
789
1127
  let estimatedBridgeGas;
790
- if (allowanceWei >= input.amountWei) {
791
- const data = (0, import_viem6.encodeFunctionData)({
792
- abi: spokeAbi,
793
- functionName: "bridgeToFinney",
794
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
795
- });
1128
+ if (allowanceWei >= input.amountWei + cut) {
1129
+ const data = encodeSpokeBridge(
1130
+ asset.evmToken,
1131
+ input.amountWei,
1132
+ exit,
1133
+ partnerFee
1134
+ );
796
1135
  estimatedBridgeGas = await provider.estimateGas({
797
1136
  account: sender,
798
1137
  to: evm.contracts.gateway,
@@ -809,6 +1148,7 @@ async function prepareEvmToSubtensor(provider, input) {
809
1148
  return Object.freeze({
810
1149
  exactNetworkFeeWei,
811
1150
  transactionValueWei: feeWithBuffer(exactNetworkFeeWei),
1151
+ partnerFeeWei: cut,
812
1152
  plan
813
1153
  });
814
1154
  }
@@ -818,58 +1158,68 @@ function prepareBaseToSubtensor(provider, input) {
818
1158
  async function prepareSubtensorToEvm(provider, input) {
819
1159
  const sender = normalizeEvmAddress(input.sender);
820
1160
  const recipient = normalizeEvmAddress(input.recipient);
1161
+ const asset = bridgeAsset(input.evmChain, input.asset);
821
1162
  assertSource(input.source);
822
- assertWholeRao(input.amountWei);
1163
+ assertAssetMode(input.asset, input.source);
1164
+ const netuid = sourceNetuid(input);
1165
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
823
1166
  const { subtensor } = foreverMoneyDeployment;
824
1167
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
825
- const exactNetworkFeeWei = await provider.readContract({
1168
+ const partnerFee = resolvePartnerFee(
1169
+ input.partnerFee,
1170
+ subtensor.contracts.gateway
1171
+ );
1172
+ const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
1173
+ const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
1174
+ const exactNetworkFeeWei = partnerFee.bps === 0 ? await provider.readContract({
826
1175
  address: subtensor.contracts.gateway,
827
1176
  abi: alphaAbi,
828
1177
  functionName: "quoteBridgeOut",
829
1178
  args: [
830
1179
  evm.ccipSelector,
831
- subtensor.contracts.wrappedTao,
1180
+ asset.subtensorToken,
832
1181
  recipient,
833
1182
  input.amountWei
834
1183
  ]
835
- });
1184
+ }) : (await quoteHubWithFee(
1185
+ provider,
1186
+ subtensor.contracts.gateway,
1187
+ evm.ccipSelector,
1188
+ asset.subtensorToken,
1189
+ recipient,
1190
+ input.amountWei,
1191
+ taoAmount,
1192
+ stakedAlphaRao,
1193
+ partnerFee
1194
+ )).networkFee;
836
1195
  let stakingAllowanceRao;
837
1196
  if (input.source === "staked") {
838
- if (input.netuid === void 0) {
839
- throw new ForeverMoneyError(
840
- "INVALID_TRANSACTION_PLAN",
841
- "A netuid is required when bridging staked TAO."
842
- );
843
- }
844
- assertNonNegativeAmount(input.netuid, "netuid");
845
1197
  stakingAllowanceRao = await provider.readContract({
846
1198
  address: subtensor.contracts.stakingPrecompile,
847
1199
  abi: stakingAbi,
848
1200
  functionName: "allowance",
849
- args: [sender, subtensor.contracts.gateway, input.netuid]
1201
+ args: [sender, subtensor.contracts.gateway, netuid]
850
1202
  });
851
1203
  }
852
- const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
853
- const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
854
- const value = taoAmount + feeWithBuffer(exactNetworkFeeWei);
1204
+ const alphaTopUpRao = partnerFeeCut(stakedAlphaRao, partnerFee.bps);
1205
+ const taoTopUp = partnerFeeTaoTopUp(taoAmount, partnerFee.bps);
1206
+ const value = taoAmount + taoTopUp + feeWithBuffer(exactNetworkFeeWei);
855
1207
  assertNonNegativeAmount(value, "Transaction value");
856
1208
  let estimatedBridgeGas;
857
- if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao) {
1209
+ if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao + alphaTopUpRao) {
858
1210
  estimatedBridgeGas = await provider.estimateGas({
859
1211
  account: sender,
860
1212
  to: subtensor.contracts.gateway,
861
- data: (0, import_viem6.encodeFunctionData)({
862
- abi: alphaAbi,
863
- functionName: "bridgeOut",
864
- args: [
865
- evm.ccipSelector,
866
- subtensor.contracts.wrappedTao,
867
- recipient,
868
- taoAmount,
869
- stakedAlphaRao,
870
- input.amountWei
871
- ]
872
- }),
1213
+ data: encodeHubBridge(
1214
+ evm.ccipSelector,
1215
+ asset.subtensorToken,
1216
+ recipient,
1217
+ taoAmount,
1218
+ stakedAlphaRao,
1219
+ input.amountWei,
1220
+ partnerFee,
1221
+ stakePullsFor(input, stakedAlphaRao)
1222
+ ),
873
1223
  value
874
1224
  });
875
1225
  }
@@ -882,6 +1232,7 @@ async function prepareSubtensorToEvm(provider, input) {
882
1232
  return Object.freeze({
883
1233
  exactNetworkFeeWei,
884
1234
  transactionValueWei: value,
1235
+ partnerFeeWei: input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
885
1236
  plan
886
1237
  });
887
1238
  }
@@ -1469,13 +1820,20 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1469
1820
  evmChainFromBridgeDirection(direction)
1470
1821
  );
1471
1822
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1472
- const [address, contractAbi, eventName] = evmToSubtensor ? [evm.contracts.gateway, spokeGatewayAbi, "BridgedToFinney"] : [
1473
- foreverMoneyDeployment.subtensor.contracts.gateway,
1823
+ const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1824
+ [...evm.contracts.legacyGateways, evm.contracts.gateway],
1825
+ spokeGatewayAbi,
1826
+ "BridgedToFinney"
1827
+ ] : [
1828
+ [
1829
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1830
+ foreverMoneyDeployment.subtensor.contracts.gateway
1831
+ ],
1474
1832
  alphaGatewayAbi,
1475
1833
  "BridgedOut"
1476
1834
  ];
1477
1835
  for (const log of receipt.logs) {
1478
- if (!isLogFrom(log, address)) continue;
1836
+ if (!addresses.some((address) => isLogFrom(log, address))) continue;
1479
1837
  try {
1480
1838
  const parsed = (0, import_viem10.decodeEventLog)({
1481
1839
  abi: contractAbi,
@@ -1629,16 +1987,23 @@ async function getCcipDeliveryStatus(provider, input) {
1629
1987
  "The CCIP execution receipt is unavailable."
1630
1988
  );
1631
1989
  }
1990
+ const subtensorGateways = [
1991
+ ...foreverMoneyDeployment.subtensor.contracts.legacyGateways,
1992
+ foreverMoneyDeployment.subtensor.contracts.gateway
1993
+ ];
1632
1994
  for (const log of receipt.logs) {
1633
- if (log.address.toLowerCase() !== foreverMoneyDeployment.subtensor.contracts.gateway.toLowerCase()) {
1995
+ if (!subtensorGateways.some(
1996
+ (gateway) => log.address.toLowerCase() === gateway.toLowerCase()
1997
+ )) {
1634
1998
  continue;
1635
1999
  }
1636
2000
  try {
1637
- if ((0, import_viem11.decodeEventLog)({
2001
+ const { eventName } = (0, import_viem11.decodeEventLog)({
1638
2002
  abi: alphaGatewayAbi2,
1639
2003
  data: log.data,
1640
2004
  topics: log.topics
1641
- }).eventName === "Claimable") {
2005
+ });
2006
+ if (eventName === "Claimable" || eventName === "NotDelivered") {
1642
2007
  return "recovery";
1643
2008
  }
1644
2009
  } catch {