@forevermoney/sdk 0.2.0 → 0.4.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,15 @@ __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,
30
31
  MIN_LIQUID_BASE_TO_SUBTENSOR_WEI: () => MIN_LIQUID_BASE_TO_SUBTENSOR_WEI,
31
32
  MIN_LIQUID_EVM_TO_SUBTENSOR_WEI: () => MIN_LIQUID_EVM_TO_SUBTENSOR_WEI,
33
+ MIN_LIQUID_SUBTENSOR_TO_EVM_WEI: () => MIN_LIQUID_SUBTENSOR_TO_EVM_WEI,
32
34
  NETWORK_FEE_BUFFER_BPS: () => NETWORK_FEE_BUFFER_BPS,
33
35
  RAO_PER_TAO: () => RAO_PER_TAO,
34
36
  ROBINHOOD_CCIP_SELECTOR: () => ROBINHOOD_CCIP_SELECTOR,
35
37
  ROBINHOOD_CHAIN_ID: () => ROBINHOOD_CHAIN_ID,
38
+ SN80_NETUID: () => SN80_NETUID,
36
39
  SUBTENSOR_CCIP_SELECTOR: () => SUBTENSOR_CCIP_SELECTOR,
37
40
  SUBTENSOR_CHAIN_ID: () => SUBTENSOR_CHAIN_ID,
38
41
  assertWholeRao: () => assertWholeRao,
@@ -65,6 +68,8 @@ __export(index_exports, {
65
68
  normalizeEvmAddress: () => normalizeEvmAddress,
66
69
  normalizeSS58: () => normalizeSS58,
67
70
  parseTaoAmount: () => parseTaoAmount,
71
+ partnerFeeCut: () => partnerFeeCut,
72
+ partnerFeeTaoTopUp: () => partnerFeeTaoTopUp,
68
73
  sourceChainId: () => sourceChainId,
69
74
  ss58ToPublicKey: () => ss58ToPublicKey,
70
75
  toEip1193Transaction: () => toEip1193Transaction,
@@ -156,7 +161,8 @@ var ROBINHOOD_CCIP_SELECTOR = 6180753054346818345n;
156
161
  var SUBTENSOR_CCIP_SELECTOR = 2135107236357186872n;
157
162
  var RAO_PER_TAO = 1000000000n;
158
163
  var EVM_WEI_PER_RAO = 1000000000n;
159
- var FOREVERMONEY_DEPLOYMENT_VERSION = "1.1.0";
164
+ var SN80_NETUID = 80n;
165
+ var FOREVERMONEY_DEPLOYMENT_VERSION = "1.2.0";
160
166
  var foreverMoneyDeployment = Object.freeze({
161
167
  version: FOREVERMONEY_DEPLOYMENT_VERSION,
162
168
  base: Object.freeze({
@@ -165,10 +171,16 @@ var foreverMoneyDeployment = Object.freeze({
165
171
  chainId: BASE_CHAIN_ID,
166
172
  ccipSelector: BASE_CCIP_SELECTOR,
167
173
  contracts: Object.freeze({
168
- gateway: (0, import_viem2.getAddress)("0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"),
174
+ gateway: (0, import_viem2.getAddress)("0x1da2415229b614C787e145D1D7346eb496319C52"),
175
+ legacyGateway: (0, import_viem2.getAddress)(
176
+ "0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"
177
+ ),
169
178
  wrappedTao: (0, import_viem2.getAddress)(
170
179
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
171
180
  ),
181
+ wrappedSn80: (0, import_viem2.getAddress)(
182
+ "0x6f63d869011f95274498023b4abfc00b30c34378"
183
+ ),
172
184
  ccipRouter: (0, import_viem2.getAddress)(
173
185
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
174
186
  ),
@@ -194,7 +206,10 @@ var foreverMoneyDeployment = Object.freeze({
194
206
  chainId: ROBINHOOD_CHAIN_ID,
195
207
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
196
208
  contracts: Object.freeze({
197
- gateway: (0, import_viem2.getAddress)("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"),
209
+ gateway: (0, import_viem2.getAddress)("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
210
+ legacyGateway: (0, import_viem2.getAddress)(
211
+ "0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"
212
+ ),
198
213
  wrappedTao: (0, import_viem2.getAddress)(
199
214
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
200
215
  ),
@@ -210,13 +225,19 @@ var foreverMoneyDeployment = Object.freeze({
210
225
  chainId: SUBTENSOR_CHAIN_ID,
211
226
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
212
227
  contracts: Object.freeze({
213
- gateway: (0, import_viem2.getAddress)("0x998f20Fea90bF7792774dECc7f994716442B1705"),
228
+ gateway: (0, import_viem2.getAddress)("0xcd0C6d98D0A126B1c113d15b4c28F38321437787"),
229
+ legacyGateway: (0, import_viem2.getAddress)(
230
+ "0x998f20Fea90bF7792774dECc7f994716442B1705"
231
+ ),
214
232
  alphaVault: (0, import_viem2.getAddress)(
215
233
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
216
234
  ),
217
235
  wrappedTao: (0, import_viem2.getAddress)(
218
236
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
219
237
  ),
238
+ wrappedSn80: (0, import_viem2.getAddress)(
239
+ "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
240
+ ),
220
241
  ccipRouter: (0, import_viem2.getAddress)(
221
242
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
222
243
  ),
@@ -397,23 +418,34 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
397
418
  "function ROUTER() view returns (address)",
398
419
  "function BITTENSOR_SELECTOR() view returns (uint64)",
399
420
  "function SUBTENSOR_GATEWAY() view returns (address)",
421
+ "function maxIntegratorFeeBps() view returns (uint16)",
422
+ "function bridgeFeeBps() view returns (uint16)",
400
423
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
401
424
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
425
+ "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)",
426
+ "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
427
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
403
428
  ]);
404
429
  var ALPHA_GATEWAY_ABI = Object.freeze([
405
430
  "function ROUTER() view returns (address)",
406
431
  "function allowedLane(uint64 selector) view returns (bool)",
432
+ "function maxIntegratorFeeBps() view returns (uint16)",
433
+ "function bridgeFeeBps() view returns (uint16)",
434
+ "function integratorTaoTopUp(uint256 taoAmount,uint16 bps) pure returns (uint256)",
407
435
  "function quoteBridgeOut(uint64 destSelector,address token,address recipient,uint256 mintedAmount) view returns (uint256 fee)",
408
436
  "function bridgeOut(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut) payable returns (bytes32 messageId)",
437
+ "function integratorCut(uint256 amount,uint16 bps) pure returns (uint256)",
438
+ "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
+ "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
409
440
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
410
441
  "function claimNative(address to)",
411
442
  "function claimStaked(address token,bytes32 destColdkey,address to)",
412
443
  "function claimToken(address token,address to)",
413
444
  "function claimableNative(address account) view returns (uint256)",
414
- "function claimableToken(address account,address token) view returns (uint256)",
445
+ "function claimableToken(address token,address account) view returns (uint256)",
415
446
  "event BridgedOut(uint64 destChainSelector,address indexed token,address indexed sender,address indexed recipient,uint256 minted,bytes32 messageId)",
416
447
  "event Claimable(address indexed token,address indexed user,uint256 native,uint256 wsn)",
448
+ "event NotDelivered(uint64 indexed sourceChainSelector,address indexed token,uint256 amount,uint8 reason)",
417
449
  "event DeliveredLiquid(address indexed token,bytes32 indexed ss58,uint256 taoOut)",
418
450
  "event DeliveredStaked(address indexed token,bytes32 indexed ss58,uint256 alphaRao)"
419
451
  ]);
@@ -571,6 +603,101 @@ var alphaAbi = (0, import_viem6.parseAbi)(ALPHA_GATEWAY_ABI);
571
603
  var stakingAbi = (0, import_viem6.parseAbi)(STAKING_ABI);
572
604
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
573
605
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
606
+ var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
607
+ var MAX_PARTNER_FEE_BPS = 1e4;
608
+ var NO_PARTNER_FEE = {
609
+ recipient: "0x0000000000000000000000000000000000000000",
610
+ bps: 0
611
+ };
612
+ function resolvePartnerFee(fee, gateway) {
613
+ if (fee === void 0) return NO_PARTNER_FEE;
614
+ if (typeof fee.bps !== "number" || !Number.isInteger(fee.bps) || fee.bps < 0 || fee.bps > MAX_PARTNER_FEE_BPS) {
615
+ throw new ForeverMoneyError(
616
+ "INVALID_PARTNER_FEE",
617
+ `Partner fee bps must be an integer between 0 and ${MAX_PARTNER_FEE_BPS}.`,
618
+ { bps: fee.bps }
619
+ );
620
+ }
621
+ if (fee.bps === 0) return NO_PARTNER_FEE;
622
+ const recipient = normalizeEvmAddress(fee.recipient);
623
+ if (recipient === NO_PARTNER_FEE.recipient || recipient.toLowerCase() === gateway.toLowerCase()) {
624
+ throw new ForeverMoneyError(
625
+ "INVALID_PARTNER_FEE",
626
+ "Partner fee recipient must not be the zero address or the gateway.",
627
+ { recipient }
628
+ );
629
+ }
630
+ return { recipient, bps: fee.bps };
631
+ }
632
+ function partnerFeeCut(amount, bps) {
633
+ return amount * BigInt(bps) / 10000n;
634
+ }
635
+ function partnerFeeTaoTopUp(taoAmount, bps) {
636
+ const raw = partnerFeeCut(taoAmount, bps);
637
+ if (raw === 0n) return 0n;
638
+ return (raw + EVM_WEI_PER_RAO - 1n) / EVM_WEI_PER_RAO * EVM_WEI_PER_RAO;
639
+ }
640
+ function assertPartnerFeeAllowed(fee, maxBps) {
641
+ if (fee.bps > maxBps) {
642
+ throw new ForeverMoneyError(
643
+ "INVALID_PARTNER_FEE",
644
+ `Partner fee of ${fee.bps} bps exceeds the gateway maximum of ${maxBps} bps.`,
645
+ { bps: fee.bps, maxBps }
646
+ );
647
+ }
648
+ }
649
+ function bridgeAsset(evmChain, asset = "tao") {
650
+ if (asset !== "tao" && asset !== "sn80") {
651
+ throw new ForeverMoneyError(
652
+ "INVALID_TRANSACTION_PLAN",
653
+ 'Asset must be "tao" or "sn80".'
654
+ );
655
+ }
656
+ if (asset === "sn80" && evmChain !== "base") {
657
+ throw new ForeverMoneyError(
658
+ "INVALID_TRANSACTION_PLAN",
659
+ "SN80 bridging is supported between Base and Subtensor."
660
+ );
661
+ }
662
+ const evm = getForeverMoneyEvmDeployment(evmChain);
663
+ return asset === "sn80" ? {
664
+ evmToken: foreverMoneyDeployment.base.contracts.wrappedSn80,
665
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedSn80,
666
+ label: "SN80",
667
+ wrappedLabel: "SN80"
668
+ } : {
669
+ evmToken: evm.contracts.wrappedTao,
670
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedTao,
671
+ label: "TAO",
672
+ wrappedLabel: "wrapped TAO"
673
+ };
674
+ }
675
+ function assertAssetMode(asset, mode) {
676
+ if (asset === "sn80" && mode !== "staked") {
677
+ throw new ForeverMoneyError(
678
+ "INVALID_TRANSACTION_PLAN",
679
+ "SN80 bridging requires staked subnet 80 input or delivery; liquid TAO conversion is not supported."
680
+ );
681
+ }
682
+ }
683
+ function sourceNetuid(input) {
684
+ if (input.source !== "staked") return void 0;
685
+ const netuid = input.netuid ?? (input.asset === "sn80" ? SN80_NETUID : void 0);
686
+ if (netuid === void 0) {
687
+ throw new ForeverMoneyError(
688
+ "INVALID_TRANSACTION_PLAN",
689
+ "A netuid is required when bridging staked TAO."
690
+ );
691
+ }
692
+ assertNonNegativeAmount(netuid, "netuid");
693
+ if (input.asset === "sn80" && netuid !== SN80_NETUID) {
694
+ throw new ForeverMoneyError(
695
+ "INVALID_TRANSACTION_PLAN",
696
+ "SN80 staking approvals must use netuid 80."
697
+ );
698
+ }
699
+ return netuid;
700
+ }
574
701
  function assertDelivery(value) {
575
702
  if (value !== "liquid" && value !== "staked") {
576
703
  throw new ForeverMoneyError(
@@ -600,6 +727,19 @@ function assertBaseToSubtensorAmount(amountWei, delivery) {
600
727
  );
601
728
  }
602
729
  }
730
+ function assertSubtensorToEvmAmount(amountWei, source) {
731
+ assertWholeRao(amountWei);
732
+ if (source === "liquid" && amountWei < MIN_LIQUID_SUBTENSOR_TO_EVM_WEI) {
733
+ throw new ForeverMoneyError(
734
+ "AMOUNT_BELOW_MINIMUM",
735
+ "Bridging liquid TAO from Subtensor requires at least 0.002 TAO.",
736
+ {
737
+ amountWei: amountWei.toString(),
738
+ minimumAmountWei: MIN_LIQUID_SUBTENSOR_TO_EVM_WEI.toString()
739
+ }
740
+ );
741
+ }
742
+ }
603
743
  function transactionStep(kind, label, chainId, from, to, data, value, gasLimit) {
604
744
  return {
605
745
  kind,
@@ -614,9 +754,94 @@ function transactionStep(kind, label, chainId, from, to, data, value, gasLimit)
614
754
  }
615
755
  };
616
756
  }
757
+ function partnerFeeSummary(charged, unit, fee) {
758
+ if (fee.bps === 0) return "";
759
+ return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
760
+ }
761
+ function encodeSpokeBridge(token, amountWei, exit, fee) {
762
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
763
+ abi: spokeAbi,
764
+ functionName: "bridgeToFinney",
765
+ args: [token, amountWei, exit]
766
+ }) : (0, import_viem6.encodeFunctionData)({
767
+ abi: spokeAbi,
768
+ functionName: "bridgeToFinneyWithFee",
769
+ args: [token, amountWei, exit, 0n, fee]
770
+ });
771
+ }
772
+ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee) {
773
+ const args = [
774
+ destSelector,
775
+ token,
776
+ recipient,
777
+ taoAmount,
778
+ stakedAlphaRao,
779
+ minTokenOut
780
+ ];
781
+ return fee.bps === 0 ? (0, import_viem6.encodeFunctionData)({
782
+ abi: alphaAbi,
783
+ functionName: "bridgeOut",
784
+ args
785
+ }) : (0, import_viem6.encodeFunctionData)({
786
+ abi: alphaAbi,
787
+ functionName: "bridgeOutWithFee",
788
+ args: [...args, fee]
789
+ });
790
+ }
791
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
792
+ const maxBps = await provider.readContract({
793
+ address: gateway,
794
+ abi: spokeAbi,
795
+ functionName: "maxIntegratorFeeBps"
796
+ });
797
+ assertPartnerFeeAllowed(fee, maxBps);
798
+ const [networkFee] = await provider.readContract({
799
+ address: gateway,
800
+ abi: spokeAbi,
801
+ functionName: "quoteBridgeToFinneyWithFee",
802
+ args: [token, amountWei, exit, 0n, fee]
803
+ });
804
+ return networkFee;
805
+ }
806
+ async function quoteHubWithFee(provider, gateway, destSelector, token, recipient, mintedAmount, taoAmount, stakedAlphaRao, fee) {
807
+ const maxBps = await provider.readContract({
808
+ address: gateway,
809
+ abi: alphaAbi,
810
+ functionName: "maxIntegratorFeeBps"
811
+ });
812
+ assertPartnerFeeAllowed(fee, maxBps);
813
+ const [networkFee, taoTopUp, alphaTopUpRao, crossing] = await provider.readContract({
814
+ address: gateway,
815
+ abi: alphaAbi,
816
+ functionName: "quoteBridgeOutWithFee",
817
+ args: [
818
+ destSelector,
819
+ token,
820
+ recipient,
821
+ mintedAmount,
822
+ taoAmount,
823
+ stakedAlphaRao,
824
+ fee
825
+ ]
826
+ });
827
+ if (crossing !== mintedAmount || taoTopUp !== partnerFeeTaoTopUp(taoAmount, fee.bps) || alphaTopUpRao !== partnerFeeCut(stakedAlphaRao, fee.bps)) {
828
+ throw new ForeverMoneyError(
829
+ "INVALID_PROVIDER_RESPONSE",
830
+ "The gateway partner fee quote does not match the SDK fee calculation.",
831
+ {
832
+ crossing: crossing.toString(),
833
+ taoTopUp: taoTopUp.toString(),
834
+ alphaTopUpRao: alphaTopUpRao.toString()
835
+ }
836
+ );
837
+ }
838
+ return { networkFee, taoTopUp, alphaTopUpRao };
839
+ }
617
840
  function buildEvmToSubtensorPlan(input) {
618
841
  const sender = normalizeEvmAddress(input.sender);
842
+ const asset = bridgeAsset(input.evmChain, input.asset);
619
843
  assertDelivery(input.delivery);
844
+ assertAssetMode(input.asset, input.delivery);
620
845
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
621
846
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
622
847
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
@@ -628,19 +853,24 @@ function buildEvmToSubtensorPlan(input) {
628
853
  minTaoOut: input.amountWei
629
854
  };
630
855
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
856
+ const partnerFee = resolvePartnerFee(
857
+ input.partnerFee,
858
+ evm.contracts.gateway
859
+ );
860
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
631
861
  const steps = [];
632
- if (input.allowanceWei < input.amountWei) {
862
+ if (input.allowanceWei < input.amountWei + cut) {
633
863
  steps.push(
634
864
  transactionStep(
635
865
  "approval",
636
- "Approve wrapped TAO for the ForeverMoney gateway",
866
+ `Approve ${asset.wrappedLabel} for the ForeverMoney gateway`,
637
867
  evm.chainId,
638
868
  sender,
639
- evm.contracts.wrappedTao,
869
+ asset.evmToken,
640
870
  (0, import_viem6.encodeFunctionData)({
641
871
  abi: erc20Abi,
642
872
  functionName: "approve",
643
- args: [evm.contracts.gateway, input.amountWei]
873
+ args: [evm.contracts.gateway, input.amountWei + cut]
644
874
  }),
645
875
  0n
646
876
  )
@@ -650,22 +880,23 @@ function buildEvmToSubtensorPlan(input) {
650
880
  steps.push(
651
881
  transactionStep(
652
882
  "transaction",
653
- `Bridge wrapped TAO from ${evm.name} to Subtensor (${input.delivery})`,
883
+ `Bridge ${asset.wrappedLabel} from ${evm.name} to Subtensor (${input.delivery})`,
654
884
  evm.chainId,
655
885
  sender,
656
886
  evm.contracts.gateway,
657
- (0, import_viem6.encodeFunctionData)({
658
- abi: spokeAbi,
659
- functionName: "bridgeToFinney",
660
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
661
- }),
887
+ encodeSpokeBridge(
888
+ asset.evmToken,
889
+ input.amountWei,
890
+ exit,
891
+ partnerFee
892
+ ),
662
893
  value,
663
894
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
664
895
  )
665
896
  );
666
897
  return createTransactionPlan({
667
898
  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}.`,
899
+ summary: `Bridge ${input.amountWei} wei of ${asset.wrappedLabel} from ${evm.name} to ${destination}.${partnerFeeSummary(cut, asset.wrappedLabel, partnerFee)}`,
669
900
  steps
670
901
  });
671
902
  }
@@ -675,21 +906,23 @@ function buildBaseToSubtensorPlan(input) {
675
906
  function buildSubtensorToEvmPlan(input) {
676
907
  const sender = normalizeEvmAddress(input.sender);
677
908
  const recipient = normalizeEvmAddress(input.recipient);
909
+ const asset = bridgeAsset(input.evmChain, input.asset);
678
910
  assertSource(input.source);
679
- assertWholeRao(input.amountWei);
911
+ assertAssetMode(input.asset, input.source);
912
+ const netuid = sourceNetuid(input);
913
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
680
914
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
681
915
  const { subtensor } = foreverMoneyDeployment;
682
916
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
917
+ const partnerFee = resolvePartnerFee(
918
+ input.partnerFee,
919
+ subtensor.contracts.gateway
920
+ );
683
921
  const amountRao = input.amountWei / EVM_WEI_PER_RAO;
922
+ const alphaTopUpRao = input.source === "staked" ? partnerFeeCut(amountRao, partnerFee.bps) : 0n;
923
+ const taoTopUp = input.source === "liquid" ? partnerFeeTaoTopUp(input.amountWei, partnerFee.bps) : 0n;
684
924
  const steps = [];
685
925
  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
926
  if (input.stakingAllowanceRao === void 0) {
694
927
  throw new ForeverMoneyError(
695
928
  "INVALID_TRANSACTION_PLAN",
@@ -697,11 +930,11 @@ function buildSubtensorToEvmPlan(input) {
697
930
  );
698
931
  }
699
932
  assertNonNegativeAmount(input.stakingAllowanceRao, "Staking allowance");
700
- if (input.stakingAllowanceRao < amountRao) {
933
+ if (input.stakingAllowanceRao < amountRao + alphaTopUpRao) {
701
934
  steps.push(
702
935
  transactionStep(
703
936
  "approval",
704
- "Approve staked TAO for the ForeverMoney gateway",
937
+ `Approve staked ${asset.label} for the ForeverMoney gateway`,
705
938
  subtensor.chainId,
706
939
  sender,
707
940
  subtensor.contracts.stakingPrecompile,
@@ -710,8 +943,8 @@ function buildSubtensorToEvmPlan(input) {
710
943
  functionName: "approve",
711
944
  args: [
712
945
  subtensor.contracts.gateway,
713
- input.netuid,
714
- amountRao
946
+ netuid,
947
+ amountRao + alphaTopUpRao
715
948
  ]
716
949
  }),
717
950
  0n
@@ -726,34 +959,35 @@ function buildSubtensorToEvmPlan(input) {
726
959
  }
727
960
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
728
961
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
729
- const value = taoAmount + feeWithBuffer(input.exactNetworkFeeWei);
962
+ const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
730
963
  assertNonNegativeAmount(value, "Transaction value");
731
964
  steps.push(
732
965
  transactionStep(
733
966
  "transaction",
734
- `Bridge ${input.source} TAO from Subtensor to ${evm.name}`,
967
+ `Bridge ${input.source} ${asset.label} from Subtensor to ${evm.name}`,
735
968
  subtensor.chainId,
736
969
  sender,
737
970
  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
- }),
971
+ encodeHubBridge(
972
+ evm.ccipSelector,
973
+ asset.subtensorToken,
974
+ recipient,
975
+ taoAmount,
976
+ stakedAlphaRao,
977
+ input.amountWei,
978
+ partnerFee
979
+ ),
750
980
  value,
751
981
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
752
982
  )
753
983
  );
754
984
  return createTransactionPlan({
755
985
  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}.`,
986
+ summary: `Bridge ${input.amountWei} wei of ${input.source} ${asset.label} from Subtensor to ${recipient} on ${evm.name}.${partnerFeeSummary(
987
+ input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
988
+ input.source === "liquid" ? "TAO" : `staked ${asset.label}`,
989
+ partnerFee
990
+ )}`,
757
991
  steps
758
992
  });
759
993
  }
@@ -762,7 +996,9 @@ function buildSubtensorToBasePlan(input) {
762
996
  }
763
997
  async function prepareEvmToSubtensor(provider, input) {
764
998
  const sender = normalizeEvmAddress(input.sender);
999
+ const asset = bridgeAsset(input.evmChain, input.asset);
765
1000
  assertDelivery(input.delivery);
1001
+ assertAssetMode(input.asset, input.delivery);
766
1002
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
767
1003
  const destination = normalizeSS58(input.destination);
768
1004
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -772,27 +1008,40 @@ async function prepareEvmToSubtensor(provider, input) {
772
1008
  wantLiquid: input.delivery === "liquid",
773
1009
  minTaoOut: input.amountWei
774
1010
  };
1011
+ const partnerFee = resolvePartnerFee(
1012
+ input.partnerFee,
1013
+ evm.contracts.gateway
1014
+ );
1015
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
775
1016
  const [allowanceWei, exactNetworkFeeWei] = await Promise.all([
776
1017
  provider.readContract({
777
- address: evm.contracts.wrappedTao,
1018
+ address: asset.evmToken,
778
1019
  abi: erc20Abi,
779
1020
  functionName: "allowance",
780
1021
  args: [sender, evm.contracts.gateway]
781
1022
  }),
782
- provider.readContract({
1023
+ partnerFee.bps === 0 ? provider.readContract({
783
1024
  address: evm.contracts.gateway,
784
1025
  abi: spokeAbi,
785
1026
  functionName: "quoteBridgeToFinney",
786
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
787
- })
1027
+ args: [asset.evmToken, input.amountWei, exit]
1028
+ }) : quoteSpokeWithFee(
1029
+ provider,
1030
+ evm.contracts.gateway,
1031
+ asset.evmToken,
1032
+ input.amountWei,
1033
+ exit,
1034
+ partnerFee
1035
+ )
788
1036
  ]);
789
1037
  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
- });
1038
+ if (allowanceWei >= input.amountWei + cut) {
1039
+ const data = encodeSpokeBridge(
1040
+ asset.evmToken,
1041
+ input.amountWei,
1042
+ exit,
1043
+ partnerFee
1044
+ );
796
1045
  estimatedBridgeGas = await provider.estimateGas({
797
1046
  account: sender,
798
1047
  to: evm.contracts.gateway,
@@ -809,6 +1058,7 @@ async function prepareEvmToSubtensor(provider, input) {
809
1058
  return Object.freeze({
810
1059
  exactNetworkFeeWei,
811
1060
  transactionValueWei: feeWithBuffer(exactNetworkFeeWei),
1061
+ partnerFeeWei: cut,
812
1062
  plan
813
1063
  });
814
1064
  }
@@ -818,58 +1068,67 @@ function prepareBaseToSubtensor(provider, input) {
818
1068
  async function prepareSubtensorToEvm(provider, input) {
819
1069
  const sender = normalizeEvmAddress(input.sender);
820
1070
  const recipient = normalizeEvmAddress(input.recipient);
1071
+ const asset = bridgeAsset(input.evmChain, input.asset);
821
1072
  assertSource(input.source);
822
- assertWholeRao(input.amountWei);
1073
+ assertAssetMode(input.asset, input.source);
1074
+ const netuid = sourceNetuid(input);
1075
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
823
1076
  const { subtensor } = foreverMoneyDeployment;
824
1077
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
825
- const exactNetworkFeeWei = await provider.readContract({
1078
+ const partnerFee = resolvePartnerFee(
1079
+ input.partnerFee,
1080
+ subtensor.contracts.gateway
1081
+ );
1082
+ const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
1083
+ const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
1084
+ const exactNetworkFeeWei = partnerFee.bps === 0 ? await provider.readContract({
826
1085
  address: subtensor.contracts.gateway,
827
1086
  abi: alphaAbi,
828
1087
  functionName: "quoteBridgeOut",
829
1088
  args: [
830
1089
  evm.ccipSelector,
831
- subtensor.contracts.wrappedTao,
1090
+ asset.subtensorToken,
832
1091
  recipient,
833
1092
  input.amountWei
834
1093
  ]
835
- });
1094
+ }) : (await quoteHubWithFee(
1095
+ provider,
1096
+ subtensor.contracts.gateway,
1097
+ evm.ccipSelector,
1098
+ asset.subtensorToken,
1099
+ recipient,
1100
+ input.amountWei,
1101
+ taoAmount,
1102
+ stakedAlphaRao,
1103
+ partnerFee
1104
+ )).networkFee;
836
1105
  let stakingAllowanceRao;
837
1106
  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
1107
  stakingAllowanceRao = await provider.readContract({
846
1108
  address: subtensor.contracts.stakingPrecompile,
847
1109
  abi: stakingAbi,
848
1110
  functionName: "allowance",
849
- args: [sender, subtensor.contracts.gateway, input.netuid]
1111
+ args: [sender, subtensor.contracts.gateway, netuid]
850
1112
  });
851
1113
  }
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);
1114
+ const alphaTopUpRao = partnerFeeCut(stakedAlphaRao, partnerFee.bps);
1115
+ const taoTopUp = partnerFeeTaoTopUp(taoAmount, partnerFee.bps);
1116
+ const value = taoAmount + taoTopUp + feeWithBuffer(exactNetworkFeeWei);
855
1117
  assertNonNegativeAmount(value, "Transaction value");
856
1118
  let estimatedBridgeGas;
857
- if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao) {
1119
+ if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao + alphaTopUpRao) {
858
1120
  estimatedBridgeGas = await provider.estimateGas({
859
1121
  account: sender,
860
1122
  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
- }),
1123
+ data: encodeHubBridge(
1124
+ evm.ccipSelector,
1125
+ asset.subtensorToken,
1126
+ recipient,
1127
+ taoAmount,
1128
+ stakedAlphaRao,
1129
+ input.amountWei,
1130
+ partnerFee
1131
+ ),
873
1132
  value
874
1133
  });
875
1134
  }
@@ -882,6 +1141,7 @@ async function prepareSubtensorToEvm(provider, input) {
882
1141
  return Object.freeze({
883
1142
  exactNetworkFeeWei,
884
1143
  transactionValueWei: value,
1144
+ partnerFeeWei: input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
885
1145
  plan
886
1146
  });
887
1147
  }
@@ -1469,13 +1729,20 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1469
1729
  evmChainFromBridgeDirection(direction)
1470
1730
  );
1471
1731
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1472
- const [address, contractAbi, eventName] = evmToSubtensor ? [evm.contracts.gateway, spokeGatewayAbi, "BridgedToFinney"] : [
1473
- foreverMoneyDeployment.subtensor.contracts.gateway,
1732
+ const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1733
+ [evm.contracts.legacyGateway, evm.contracts.gateway],
1734
+ spokeGatewayAbi,
1735
+ "BridgedToFinney"
1736
+ ] : [
1737
+ [
1738
+ foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1739
+ foreverMoneyDeployment.subtensor.contracts.gateway
1740
+ ],
1474
1741
  alphaGatewayAbi,
1475
1742
  "BridgedOut"
1476
1743
  ];
1477
1744
  for (const log of receipt.logs) {
1478
- if (!isLogFrom(log, address)) continue;
1745
+ if (!addresses.some((address) => isLogFrom(log, address))) continue;
1479
1746
  try {
1480
1747
  const parsed = (0, import_viem10.decodeEventLog)({
1481
1748
  abi: contractAbi,
@@ -1629,16 +1896,23 @@ async function getCcipDeliveryStatus(provider, input) {
1629
1896
  "The CCIP execution receipt is unavailable."
1630
1897
  );
1631
1898
  }
1899
+ const subtensorGateways = [
1900
+ foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1901
+ foreverMoneyDeployment.subtensor.contracts.gateway
1902
+ ];
1632
1903
  for (const log of receipt.logs) {
1633
- if (log.address.toLowerCase() !== foreverMoneyDeployment.subtensor.contracts.gateway.toLowerCase()) {
1904
+ if (!subtensorGateways.some(
1905
+ (gateway) => log.address.toLowerCase() === gateway.toLowerCase()
1906
+ )) {
1634
1907
  continue;
1635
1908
  }
1636
1909
  try {
1637
- if ((0, import_viem11.decodeEventLog)({
1910
+ const { eventName } = (0, import_viem11.decodeEventLog)({
1638
1911
  abi: alphaGatewayAbi2,
1639
1912
  data: log.data,
1640
1913
  topics: log.topics
1641
- }).eventName === "Claimable") {
1914
+ });
1915
+ if (eventName === "Claimable" || eventName === "NotDelivered") {
1642
1916
  return "recovery";
1643
1917
  }
1644
1918
  } catch {