@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.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.2.0";
96
97
  var foreverMoneyDeployment = Object.freeze({
97
98
  version: FOREVERMONEY_DEPLOYMENT_VERSION,
98
99
  base: Object.freeze({
@@ -101,10 +102,16 @@ 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
+ legacyGateway: getAddress2(
107
+ "0x5EF3d7D19e4b233a1A169DA0d5CB02ec6b160a2C"
108
+ ),
105
109
  wrappedTao: getAddress2(
106
110
  "0xf3081494b87e8d5fb7960f066e931d1d0e6e3d67"
107
111
  ),
112
+ wrappedSn80: getAddress2(
113
+ "0x6f63d869011f95274498023b4abfc00b30c34378"
114
+ ),
108
115
  ccipRouter: getAddress2(
109
116
  "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"
110
117
  ),
@@ -130,7 +137,10 @@ var foreverMoneyDeployment = Object.freeze({
130
137
  chainId: ROBINHOOD_CHAIN_ID,
131
138
  ccipSelector: ROBINHOOD_CCIP_SELECTOR,
132
139
  contracts: Object.freeze({
133
- gateway: getAddress2("0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"),
140
+ gateway: getAddress2("0xf27fdA637131E25B2A1b4865ED9597d881980c7E"),
141
+ legacyGateway: getAddress2(
142
+ "0x53Dcc4FE04193e489BE537F722F65317DB1E65d8"
143
+ ),
134
144
  wrappedTao: getAddress2(
135
145
  "0xf3081494B87e8D5fb7960f066E931D1D0e6E3d67"
136
146
  ),
@@ -146,13 +156,19 @@ var foreverMoneyDeployment = Object.freeze({
146
156
  chainId: SUBTENSOR_CHAIN_ID,
147
157
  ccipSelector: SUBTENSOR_CCIP_SELECTOR,
148
158
  contracts: Object.freeze({
149
- gateway: getAddress2("0x998f20Fea90bF7792774dECc7f994716442B1705"),
159
+ gateway: getAddress2("0xcd0C6d98D0A126B1c113d15b4c28F38321437787"),
160
+ legacyGateway: getAddress2(
161
+ "0x998f20Fea90bF7792774dECc7f994716442B1705"
162
+ ),
150
163
  alphaVault: getAddress2(
151
164
  "0x11837459896D96F821a8D88eC93a3C8D152033D4"
152
165
  ),
153
166
  wrappedTao: getAddress2(
154
167
  "0xC5b6C1632d34901239396F5E1BDe54B342900256"
155
168
  ),
169
+ wrappedSn80: getAddress2(
170
+ "0xfD628dE75EF96f0A5C59659159C6cA81E0DC2222"
171
+ ),
156
172
  ccipRouter: getAddress2(
157
173
  "0xD941fBEcD2b971d0F54b4C34286C95faB52B60B8"
158
174
  ),
@@ -333,23 +349,34 @@ var SPOKE_GATEWAY_ABI = Object.freeze([
333
349
  "function ROUTER() view returns (address)",
334
350
  "function BITTENSOR_SELECTOR() view returns (uint64)",
335
351
  "function SUBTENSOR_GATEWAY() view returns (address)",
352
+ "function maxIntegratorFeeBps() view returns (uint16)",
353
+ "function bridgeFeeBps() view returns (uint16)",
336
354
  "function quoteBridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) view returns (uint256 fee)",
337
355
  "function bridgeToFinney(address token,uint256 amount,(bytes32 ss58,address evmFallback,bool wantLiquid,uint256 minTaoOut) exit) payable returns (bytes32 messageId)",
356
+ "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)",
357
+ "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
358
  "event BridgedToFinney(address indexed token,address indexed sender,bytes32 indexed ss58,uint256 amount,bytes32 messageId)"
339
359
  ]);
340
360
  var ALPHA_GATEWAY_ABI = Object.freeze([
341
361
  "function ROUTER() view returns (address)",
342
362
  "function allowedLane(uint64 selector) view returns (bool)",
363
+ "function maxIntegratorFeeBps() view returns (uint16)",
364
+ "function bridgeFeeBps() view returns (uint16)",
365
+ "function integratorTaoTopUp(uint256 taoAmount,uint16 bps) pure returns (uint256)",
343
366
  "function quoteBridgeOut(uint64 destSelector,address token,address recipient,uint256 mintedAmount) view returns (uint256 fee)",
344
367
  "function bridgeOut(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut) payable returns (bytes32 messageId)",
368
+ "function integratorCut(uint256 amount,uint16 bps) pure returns (uint256)",
369
+ "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)",
370
+ "function bridgeOutWithFee(uint64 destSelector,address token,address recipient,uint256 taoAmount,uint256 stakedAlphaRao,uint256 minTokenOut,(address recipient,uint16 bps) integrator) payable returns (bytes32 messageId)",
345
371
  "function claimLiquid(address token,uint256 minTaoOut,address to)",
346
372
  "function claimNative(address to)",
347
373
  "function claimStaked(address token,bytes32 destColdkey,address to)",
348
374
  "function claimToken(address token,address to)",
349
375
  "function claimableNative(address account) view returns (uint256)",
350
- "function claimableToken(address account,address token) view returns (uint256)",
376
+ "function claimableToken(address token,address account) view returns (uint256)",
351
377
  "event BridgedOut(uint64 destChainSelector,address indexed token,address indexed sender,address indexed recipient,uint256 minted,bytes32 messageId)",
352
378
  "event Claimable(address indexed token,address indexed user,uint256 native,uint256 wsn)",
379
+ "event NotDelivered(uint64 indexed sourceChainSelector,address indexed token,uint256 amount,uint8 reason)",
353
380
  "event DeliveredLiquid(address indexed token,bytes32 indexed ss58,uint256 taoOut)",
354
381
  "event DeliveredStaked(address indexed token,bytes32 indexed ss58,uint256 alphaRao)"
355
382
  ]);
@@ -507,6 +534,101 @@ var alphaAbi = parseAbi(ALPHA_GATEWAY_ABI);
507
534
  var stakingAbi = parseAbi(STAKING_ABI);
508
535
  var MIN_LIQUID_EVM_TO_SUBTENSOR_WEI = 10000000000000000n;
509
536
  var MIN_LIQUID_BASE_TO_SUBTENSOR_WEI = MIN_LIQUID_EVM_TO_SUBTENSOR_WEI;
537
+ var MIN_LIQUID_SUBTENSOR_TO_EVM_WEI = 2000000n * EVM_WEI_PER_RAO;
538
+ var MAX_PARTNER_FEE_BPS = 1e4;
539
+ var NO_PARTNER_FEE = {
540
+ recipient: "0x0000000000000000000000000000000000000000",
541
+ bps: 0
542
+ };
543
+ function resolvePartnerFee(fee, gateway) {
544
+ if (fee === void 0) return NO_PARTNER_FEE;
545
+ if (typeof fee.bps !== "number" || !Number.isInteger(fee.bps) || fee.bps < 0 || fee.bps > MAX_PARTNER_FEE_BPS) {
546
+ throw new ForeverMoneyError(
547
+ "INVALID_PARTNER_FEE",
548
+ `Partner fee bps must be an integer between 0 and ${MAX_PARTNER_FEE_BPS}.`,
549
+ { bps: fee.bps }
550
+ );
551
+ }
552
+ if (fee.bps === 0) return NO_PARTNER_FEE;
553
+ const recipient = normalizeEvmAddress(fee.recipient);
554
+ if (recipient === NO_PARTNER_FEE.recipient || recipient.toLowerCase() === gateway.toLowerCase()) {
555
+ throw new ForeverMoneyError(
556
+ "INVALID_PARTNER_FEE",
557
+ "Partner fee recipient must not be the zero address or the gateway.",
558
+ { recipient }
559
+ );
560
+ }
561
+ return { recipient, bps: fee.bps };
562
+ }
563
+ function partnerFeeCut(amount, bps) {
564
+ return amount * BigInt(bps) / 10000n;
565
+ }
566
+ function partnerFeeTaoTopUp(taoAmount, bps) {
567
+ const raw = partnerFeeCut(taoAmount, bps);
568
+ if (raw === 0n) return 0n;
569
+ return (raw + EVM_WEI_PER_RAO - 1n) / EVM_WEI_PER_RAO * EVM_WEI_PER_RAO;
570
+ }
571
+ function assertPartnerFeeAllowed(fee, maxBps) {
572
+ if (fee.bps > maxBps) {
573
+ throw new ForeverMoneyError(
574
+ "INVALID_PARTNER_FEE",
575
+ `Partner fee of ${fee.bps} bps exceeds the gateway maximum of ${maxBps} bps.`,
576
+ { bps: fee.bps, maxBps }
577
+ );
578
+ }
579
+ }
580
+ function bridgeAsset(evmChain, asset = "tao") {
581
+ if (asset !== "tao" && asset !== "sn80") {
582
+ throw new ForeverMoneyError(
583
+ "INVALID_TRANSACTION_PLAN",
584
+ 'Asset must be "tao" or "sn80".'
585
+ );
586
+ }
587
+ if (asset === "sn80" && evmChain !== "base") {
588
+ throw new ForeverMoneyError(
589
+ "INVALID_TRANSACTION_PLAN",
590
+ "SN80 bridging is supported between Base and Subtensor."
591
+ );
592
+ }
593
+ const evm = getForeverMoneyEvmDeployment(evmChain);
594
+ return asset === "sn80" ? {
595
+ evmToken: foreverMoneyDeployment.base.contracts.wrappedSn80,
596
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedSn80,
597
+ label: "SN80",
598
+ wrappedLabel: "SN80"
599
+ } : {
600
+ evmToken: evm.contracts.wrappedTao,
601
+ subtensorToken: foreverMoneyDeployment.subtensor.contracts.wrappedTao,
602
+ label: "TAO",
603
+ wrappedLabel: "wrapped TAO"
604
+ };
605
+ }
606
+ function assertAssetMode(asset, mode) {
607
+ if (asset === "sn80" && mode !== "staked") {
608
+ throw new ForeverMoneyError(
609
+ "INVALID_TRANSACTION_PLAN",
610
+ "SN80 bridging requires staked subnet 80 input or delivery; liquid TAO conversion is not supported."
611
+ );
612
+ }
613
+ }
614
+ function sourceNetuid(input) {
615
+ if (input.source !== "staked") return void 0;
616
+ const netuid = input.netuid ?? (input.asset === "sn80" ? SN80_NETUID : void 0);
617
+ if (netuid === void 0) {
618
+ throw new ForeverMoneyError(
619
+ "INVALID_TRANSACTION_PLAN",
620
+ "A netuid is required when bridging staked TAO."
621
+ );
622
+ }
623
+ assertNonNegativeAmount(netuid, "netuid");
624
+ if (input.asset === "sn80" && netuid !== SN80_NETUID) {
625
+ throw new ForeverMoneyError(
626
+ "INVALID_TRANSACTION_PLAN",
627
+ "SN80 staking approvals must use netuid 80."
628
+ );
629
+ }
630
+ return netuid;
631
+ }
510
632
  function assertDelivery(value) {
511
633
  if (value !== "liquid" && value !== "staked") {
512
634
  throw new ForeverMoneyError(
@@ -536,6 +658,19 @@ function assertBaseToSubtensorAmount(amountWei, delivery) {
536
658
  );
537
659
  }
538
660
  }
661
+ function assertSubtensorToEvmAmount(amountWei, source) {
662
+ assertWholeRao(amountWei);
663
+ if (source === "liquid" && amountWei < MIN_LIQUID_SUBTENSOR_TO_EVM_WEI) {
664
+ throw new ForeverMoneyError(
665
+ "AMOUNT_BELOW_MINIMUM",
666
+ "Bridging liquid TAO from Subtensor requires at least 0.002 TAO.",
667
+ {
668
+ amountWei: amountWei.toString(),
669
+ minimumAmountWei: MIN_LIQUID_SUBTENSOR_TO_EVM_WEI.toString()
670
+ }
671
+ );
672
+ }
673
+ }
539
674
  function transactionStep(kind, label, chainId, from, to, data, value, gasLimit) {
540
675
  return {
541
676
  kind,
@@ -550,9 +685,94 @@ function transactionStep(kind, label, chainId, from, to, data, value, gasLimit)
550
685
  }
551
686
  };
552
687
  }
688
+ function partnerFeeSummary(charged, unit, fee) {
689
+ if (fee.bps === 0) return "";
690
+ return ` Partner fee: ${charged} wei of ${unit} (${fee.bps} bps) on top, paid to ${fee.recipient}.`;
691
+ }
692
+ function encodeSpokeBridge(token, amountWei, exit, fee) {
693
+ return fee.bps === 0 ? encodeFunctionData({
694
+ abi: spokeAbi,
695
+ functionName: "bridgeToFinney",
696
+ args: [token, amountWei, exit]
697
+ }) : encodeFunctionData({
698
+ abi: spokeAbi,
699
+ functionName: "bridgeToFinneyWithFee",
700
+ args: [token, amountWei, exit, 0n, fee]
701
+ });
702
+ }
703
+ function encodeHubBridge(destSelector, token, recipient, taoAmount, stakedAlphaRao, minTokenOut, fee) {
704
+ const args = [
705
+ destSelector,
706
+ token,
707
+ recipient,
708
+ taoAmount,
709
+ stakedAlphaRao,
710
+ minTokenOut
711
+ ];
712
+ return fee.bps === 0 ? encodeFunctionData({
713
+ abi: alphaAbi,
714
+ functionName: "bridgeOut",
715
+ args
716
+ }) : encodeFunctionData({
717
+ abi: alphaAbi,
718
+ functionName: "bridgeOutWithFee",
719
+ args: [...args, fee]
720
+ });
721
+ }
722
+ async function quoteSpokeWithFee(provider, gateway, token, amountWei, exit, fee) {
723
+ const maxBps = await provider.readContract({
724
+ address: gateway,
725
+ abi: spokeAbi,
726
+ functionName: "maxIntegratorFeeBps"
727
+ });
728
+ assertPartnerFeeAllowed(fee, maxBps);
729
+ const [networkFee] = await provider.readContract({
730
+ address: gateway,
731
+ abi: spokeAbi,
732
+ functionName: "quoteBridgeToFinneyWithFee",
733
+ args: [token, amountWei, exit, 0n, fee]
734
+ });
735
+ return networkFee;
736
+ }
737
+ async function quoteHubWithFee(provider, gateway, destSelector, token, recipient, mintedAmount, taoAmount, stakedAlphaRao, fee) {
738
+ const maxBps = await provider.readContract({
739
+ address: gateway,
740
+ abi: alphaAbi,
741
+ functionName: "maxIntegratorFeeBps"
742
+ });
743
+ assertPartnerFeeAllowed(fee, maxBps);
744
+ const [networkFee, taoTopUp, alphaTopUpRao, crossing] = await provider.readContract({
745
+ address: gateway,
746
+ abi: alphaAbi,
747
+ functionName: "quoteBridgeOutWithFee",
748
+ args: [
749
+ destSelector,
750
+ token,
751
+ recipient,
752
+ mintedAmount,
753
+ taoAmount,
754
+ stakedAlphaRao,
755
+ fee
756
+ ]
757
+ });
758
+ if (crossing !== mintedAmount || taoTopUp !== partnerFeeTaoTopUp(taoAmount, fee.bps) || alphaTopUpRao !== partnerFeeCut(stakedAlphaRao, fee.bps)) {
759
+ throw new ForeverMoneyError(
760
+ "INVALID_PROVIDER_RESPONSE",
761
+ "The gateway partner fee quote does not match the SDK fee calculation.",
762
+ {
763
+ crossing: crossing.toString(),
764
+ taoTopUp: taoTopUp.toString(),
765
+ alphaTopUpRao: alphaTopUpRao.toString()
766
+ }
767
+ );
768
+ }
769
+ return { networkFee, taoTopUp, alphaTopUpRao };
770
+ }
553
771
  function buildEvmToSubtensorPlan(input) {
554
772
  const sender = normalizeEvmAddress(input.sender);
773
+ const asset = bridgeAsset(input.evmChain, input.asset);
555
774
  assertDelivery(input.delivery);
775
+ assertAssetMode(input.asset, input.delivery);
556
776
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
557
777
  assertNonNegativeAmount(input.allowanceWei, "Token allowance");
558
778
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
@@ -564,19 +784,24 @@ function buildEvmToSubtensorPlan(input) {
564
784
  minTaoOut: input.amountWei
565
785
  };
566
786
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
787
+ const partnerFee = resolvePartnerFee(
788
+ input.partnerFee,
789
+ evm.contracts.gateway
790
+ );
791
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
567
792
  const steps = [];
568
- if (input.allowanceWei < input.amountWei) {
793
+ if (input.allowanceWei < input.amountWei + cut) {
569
794
  steps.push(
570
795
  transactionStep(
571
796
  "approval",
572
- "Approve wrapped TAO for the ForeverMoney gateway",
797
+ `Approve ${asset.wrappedLabel} for the ForeverMoney gateway`,
573
798
  evm.chainId,
574
799
  sender,
575
- evm.contracts.wrappedTao,
800
+ asset.evmToken,
576
801
  encodeFunctionData({
577
802
  abi: erc20Abi,
578
803
  functionName: "approve",
579
- args: [evm.contracts.gateway, input.amountWei]
804
+ args: [evm.contracts.gateway, input.amountWei + cut]
580
805
  }),
581
806
  0n
582
807
  )
@@ -586,22 +811,23 @@ function buildEvmToSubtensorPlan(input) {
586
811
  steps.push(
587
812
  transactionStep(
588
813
  "transaction",
589
- `Bridge wrapped TAO from ${evm.name} to Subtensor (${input.delivery})`,
814
+ `Bridge ${asset.wrappedLabel} from ${evm.name} to Subtensor (${input.delivery})`,
590
815
  evm.chainId,
591
816
  sender,
592
817
  evm.contracts.gateway,
593
- encodeFunctionData({
594
- abi: spokeAbi,
595
- functionName: "bridgeToFinney",
596
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
597
- }),
818
+ encodeSpokeBridge(
819
+ asset.evmToken,
820
+ input.amountWei,
821
+ exit,
822
+ partnerFee
823
+ ),
598
824
  value,
599
825
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
600
826
  )
601
827
  );
602
828
  return createTransactionPlan({
603
829
  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}.`,
830
+ summary: `Bridge ${input.amountWei} wei of ${asset.wrappedLabel} from ${evm.name} to ${destination}.${partnerFeeSummary(cut, asset.wrappedLabel, partnerFee)}`,
605
831
  steps
606
832
  });
607
833
  }
@@ -611,21 +837,23 @@ function buildBaseToSubtensorPlan(input) {
611
837
  function buildSubtensorToEvmPlan(input) {
612
838
  const sender = normalizeEvmAddress(input.sender);
613
839
  const recipient = normalizeEvmAddress(input.recipient);
840
+ const asset = bridgeAsset(input.evmChain, input.asset);
614
841
  assertSource(input.source);
615
- assertWholeRao(input.amountWei);
842
+ assertAssetMode(input.asset, input.source);
843
+ const netuid = sourceNetuid(input);
844
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
616
845
  assertNonNegativeAmount(input.exactNetworkFeeWei, "Network fee");
617
846
  const { subtensor } = foreverMoneyDeployment;
618
847
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
848
+ const partnerFee = resolvePartnerFee(
849
+ input.partnerFee,
850
+ subtensor.contracts.gateway
851
+ );
619
852
  const amountRao = input.amountWei / EVM_WEI_PER_RAO;
853
+ const alphaTopUpRao = input.source === "staked" ? partnerFeeCut(amountRao, partnerFee.bps) : 0n;
854
+ const taoTopUp = input.source === "liquid" ? partnerFeeTaoTopUp(input.amountWei, partnerFee.bps) : 0n;
620
855
  const steps = [];
621
856
  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
857
  if (input.stakingAllowanceRao === void 0) {
630
858
  throw new ForeverMoneyError(
631
859
  "INVALID_TRANSACTION_PLAN",
@@ -633,11 +861,11 @@ function buildSubtensorToEvmPlan(input) {
633
861
  );
634
862
  }
635
863
  assertNonNegativeAmount(input.stakingAllowanceRao, "Staking allowance");
636
- if (input.stakingAllowanceRao < amountRao) {
864
+ if (input.stakingAllowanceRao < amountRao + alphaTopUpRao) {
637
865
  steps.push(
638
866
  transactionStep(
639
867
  "approval",
640
- "Approve staked TAO for the ForeverMoney gateway",
868
+ `Approve staked ${asset.label} for the ForeverMoney gateway`,
641
869
  subtensor.chainId,
642
870
  sender,
643
871
  subtensor.contracts.stakingPrecompile,
@@ -646,8 +874,8 @@ function buildSubtensorToEvmPlan(input) {
646
874
  functionName: "approve",
647
875
  args: [
648
876
  subtensor.contracts.gateway,
649
- input.netuid,
650
- amountRao
877
+ netuid,
878
+ amountRao + alphaTopUpRao
651
879
  ]
652
880
  }),
653
881
  0n
@@ -662,34 +890,35 @@ function buildSubtensorToEvmPlan(input) {
662
890
  }
663
891
  const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
664
892
  const stakedAlphaRao = input.source === "staked" ? amountRao : 0n;
665
- const value = taoAmount + feeWithBuffer(input.exactNetworkFeeWei);
893
+ const value = taoAmount + taoTopUp + feeWithBuffer(input.exactNetworkFeeWei);
666
894
  assertNonNegativeAmount(value, "Transaction value");
667
895
  steps.push(
668
896
  transactionStep(
669
897
  "transaction",
670
- `Bridge ${input.source} TAO from Subtensor to ${evm.name}`,
898
+ `Bridge ${input.source} ${asset.label} from Subtensor to ${evm.name}`,
671
899
  subtensor.chainId,
672
900
  sender,
673
901
  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
- }),
902
+ encodeHubBridge(
903
+ evm.ccipSelector,
904
+ asset.subtensorToken,
905
+ recipient,
906
+ taoAmount,
907
+ stakedAlphaRao,
908
+ input.amountWei,
909
+ partnerFee
910
+ ),
686
911
  value,
687
912
  input.estimatedBridgeGas === void 0 ? void 0 : gasLimitWithBuffer(input.estimatedBridgeGas)
688
913
  )
689
914
  );
690
915
  return createTransactionPlan({
691
916
  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}.`,
917
+ summary: `Bridge ${input.amountWei} wei of ${input.source} ${asset.label} from Subtensor to ${recipient} on ${evm.name}.${partnerFeeSummary(
918
+ input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
919
+ input.source === "liquid" ? "TAO" : `staked ${asset.label}`,
920
+ partnerFee
921
+ )}`,
693
922
  steps
694
923
  });
695
924
  }
@@ -698,7 +927,9 @@ function buildSubtensorToBasePlan(input) {
698
927
  }
699
928
  async function prepareEvmToSubtensor(provider, input) {
700
929
  const sender = normalizeEvmAddress(input.sender);
930
+ const asset = bridgeAsset(input.evmChain, input.asset);
701
931
  assertDelivery(input.delivery);
932
+ assertAssetMode(input.asset, input.delivery);
702
933
  assertBaseToSubtensorAmount(input.amountWei, input.delivery);
703
934
  const destination = normalizeSS58(input.destination);
704
935
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
@@ -708,27 +939,40 @@ async function prepareEvmToSubtensor(provider, input) {
708
939
  wantLiquid: input.delivery === "liquid",
709
940
  minTaoOut: input.amountWei
710
941
  };
942
+ const partnerFee = resolvePartnerFee(
943
+ input.partnerFee,
944
+ evm.contracts.gateway
945
+ );
946
+ const cut = partnerFeeCut(input.amountWei, partnerFee.bps);
711
947
  const [allowanceWei, exactNetworkFeeWei] = await Promise.all([
712
948
  provider.readContract({
713
- address: evm.contracts.wrappedTao,
949
+ address: asset.evmToken,
714
950
  abi: erc20Abi,
715
951
  functionName: "allowance",
716
952
  args: [sender, evm.contracts.gateway]
717
953
  }),
718
- provider.readContract({
954
+ partnerFee.bps === 0 ? provider.readContract({
719
955
  address: evm.contracts.gateway,
720
956
  abi: spokeAbi,
721
957
  functionName: "quoteBridgeToFinney",
722
- args: [evm.contracts.wrappedTao, input.amountWei, exit]
723
- })
958
+ args: [asset.evmToken, input.amountWei, exit]
959
+ }) : quoteSpokeWithFee(
960
+ provider,
961
+ evm.contracts.gateway,
962
+ asset.evmToken,
963
+ input.amountWei,
964
+ exit,
965
+ partnerFee
966
+ )
724
967
  ]);
725
968
  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
- });
969
+ if (allowanceWei >= input.amountWei + cut) {
970
+ const data = encodeSpokeBridge(
971
+ asset.evmToken,
972
+ input.amountWei,
973
+ exit,
974
+ partnerFee
975
+ );
732
976
  estimatedBridgeGas = await provider.estimateGas({
733
977
  account: sender,
734
978
  to: evm.contracts.gateway,
@@ -745,6 +989,7 @@ async function prepareEvmToSubtensor(provider, input) {
745
989
  return Object.freeze({
746
990
  exactNetworkFeeWei,
747
991
  transactionValueWei: feeWithBuffer(exactNetworkFeeWei),
992
+ partnerFeeWei: cut,
748
993
  plan
749
994
  });
750
995
  }
@@ -754,58 +999,67 @@ function prepareBaseToSubtensor(provider, input) {
754
999
  async function prepareSubtensorToEvm(provider, input) {
755
1000
  const sender = normalizeEvmAddress(input.sender);
756
1001
  const recipient = normalizeEvmAddress(input.recipient);
1002
+ const asset = bridgeAsset(input.evmChain, input.asset);
757
1003
  assertSource(input.source);
758
- assertWholeRao(input.amountWei);
1004
+ assertAssetMode(input.asset, input.source);
1005
+ const netuid = sourceNetuid(input);
1006
+ assertSubtensorToEvmAmount(input.amountWei, input.source);
759
1007
  const { subtensor } = foreverMoneyDeployment;
760
1008
  const evm = getForeverMoneyEvmDeployment(input.evmChain);
761
- const exactNetworkFeeWei = await provider.readContract({
1009
+ const partnerFee = resolvePartnerFee(
1010
+ input.partnerFee,
1011
+ subtensor.contracts.gateway
1012
+ );
1013
+ const taoAmount = input.source === "liquid" ? input.amountWei : 0n;
1014
+ const stakedAlphaRao = input.source === "staked" ? input.amountWei / EVM_WEI_PER_RAO : 0n;
1015
+ const exactNetworkFeeWei = partnerFee.bps === 0 ? await provider.readContract({
762
1016
  address: subtensor.contracts.gateway,
763
1017
  abi: alphaAbi,
764
1018
  functionName: "quoteBridgeOut",
765
1019
  args: [
766
1020
  evm.ccipSelector,
767
- subtensor.contracts.wrappedTao,
1021
+ asset.subtensorToken,
768
1022
  recipient,
769
1023
  input.amountWei
770
1024
  ]
771
- });
1025
+ }) : (await quoteHubWithFee(
1026
+ provider,
1027
+ subtensor.contracts.gateway,
1028
+ evm.ccipSelector,
1029
+ asset.subtensorToken,
1030
+ recipient,
1031
+ input.amountWei,
1032
+ taoAmount,
1033
+ stakedAlphaRao,
1034
+ partnerFee
1035
+ )).networkFee;
772
1036
  let stakingAllowanceRao;
773
1037
  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
1038
  stakingAllowanceRao = await provider.readContract({
782
1039
  address: subtensor.contracts.stakingPrecompile,
783
1040
  abi: stakingAbi,
784
1041
  functionName: "allowance",
785
- args: [sender, subtensor.contracts.gateway, input.netuid]
1042
+ args: [sender, subtensor.contracts.gateway, netuid]
786
1043
  });
787
1044
  }
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);
1045
+ const alphaTopUpRao = partnerFeeCut(stakedAlphaRao, partnerFee.bps);
1046
+ const taoTopUp = partnerFeeTaoTopUp(taoAmount, partnerFee.bps);
1047
+ const value = taoAmount + taoTopUp + feeWithBuffer(exactNetworkFeeWei);
791
1048
  assertNonNegativeAmount(value, "Transaction value");
792
1049
  let estimatedBridgeGas;
793
- if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao) {
1050
+ if (input.source === "liquid" || stakingAllowanceRao !== void 0 && stakingAllowanceRao >= stakedAlphaRao + alphaTopUpRao) {
794
1051
  estimatedBridgeGas = await provider.estimateGas({
795
1052
  account: sender,
796
1053
  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
- }),
1054
+ data: encodeHubBridge(
1055
+ evm.ccipSelector,
1056
+ asset.subtensorToken,
1057
+ recipient,
1058
+ taoAmount,
1059
+ stakedAlphaRao,
1060
+ input.amountWei,
1061
+ partnerFee
1062
+ ),
809
1063
  value
810
1064
  });
811
1065
  }
@@ -818,6 +1072,7 @@ async function prepareSubtensorToEvm(provider, input) {
818
1072
  return Object.freeze({
819
1073
  exactNetworkFeeWei,
820
1074
  transactionValueWei: value,
1075
+ partnerFeeWei: input.source === "liquid" ? taoTopUp : alphaTopUpRao * EVM_WEI_PER_RAO,
821
1076
  plan
822
1077
  });
823
1078
  }
@@ -1417,13 +1672,20 @@ function bridgeMessageIdFromReceipt(direction, receipt) {
1417
1672
  evmChainFromBridgeDirection(direction)
1418
1673
  );
1419
1674
  const evmToSubtensor = isEvmToSubtensorDirection(direction);
1420
- const [address, contractAbi, eventName] = evmToSubtensor ? [evm.contracts.gateway, spokeGatewayAbi, "BridgedToFinney"] : [
1421
- foreverMoneyDeployment.subtensor.contracts.gateway,
1675
+ const [addresses, contractAbi, eventName] = evmToSubtensor ? [
1676
+ [evm.contracts.legacyGateway, evm.contracts.gateway],
1677
+ spokeGatewayAbi,
1678
+ "BridgedToFinney"
1679
+ ] : [
1680
+ [
1681
+ foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1682
+ foreverMoneyDeployment.subtensor.contracts.gateway
1683
+ ],
1422
1684
  alphaGatewayAbi,
1423
1685
  "BridgedOut"
1424
1686
  ];
1425
1687
  for (const log of receipt.logs) {
1426
- if (!isLogFrom(log, address)) continue;
1688
+ if (!addresses.some((address) => isLogFrom(log, address))) continue;
1427
1689
  try {
1428
1690
  const parsed = decodeEventLog({
1429
1691
  abi: contractAbi,
@@ -1577,16 +1839,23 @@ async function getCcipDeliveryStatus(provider, input) {
1577
1839
  "The CCIP execution receipt is unavailable."
1578
1840
  );
1579
1841
  }
1842
+ const subtensorGateways = [
1843
+ foreverMoneyDeployment.subtensor.contracts.legacyGateway,
1844
+ foreverMoneyDeployment.subtensor.contracts.gateway
1845
+ ];
1580
1846
  for (const log of receipt.logs) {
1581
- if (log.address.toLowerCase() !== foreverMoneyDeployment.subtensor.contracts.gateway.toLowerCase()) {
1847
+ if (!subtensorGateways.some(
1848
+ (gateway) => log.address.toLowerCase() === gateway.toLowerCase()
1849
+ )) {
1582
1850
  continue;
1583
1851
  }
1584
1852
  try {
1585
- if (decodeEventLog2({
1853
+ const { eventName } = decodeEventLog2({
1586
1854
  abi: alphaGatewayAbi2,
1587
1855
  data: log.data,
1588
1856
  topics: log.topics
1589
- }).eventName === "Claimable") {
1857
+ });
1858
+ if (eventName === "Claimable" || eventName === "NotDelivered") {
1590
1859
  return "recovery";
1591
1860
  }
1592
1861
  } catch {
@@ -1872,12 +2141,15 @@ export {
1872
2141
  FOREVERMONEY_DEPLOYMENT_VERSION,
1873
2142
  ForeverMoneyError,
1874
2143
  GAS_LIMIT_BUFFER_BPS,
2144
+ MAX_PARTNER_FEE_BPS,
1875
2145
  MIN_LIQUID_BASE_TO_SUBTENSOR_WEI,
1876
2146
  MIN_LIQUID_EVM_TO_SUBTENSOR_WEI,
2147
+ MIN_LIQUID_SUBTENSOR_TO_EVM_WEI,
1877
2148
  NETWORK_FEE_BUFFER_BPS,
1878
2149
  RAO_PER_TAO,
1879
2150
  ROBINHOOD_CCIP_SELECTOR,
1880
2151
  ROBINHOOD_CHAIN_ID,
2152
+ SN80_NETUID,
1881
2153
  SUBTENSOR_CCIP_SELECTOR,
1882
2154
  SUBTENSOR_CHAIN_ID,
1883
2155
  assertWholeRao,
@@ -1910,6 +2182,8 @@ export {
1910
2182
  normalizeEvmAddress,
1911
2183
  normalizeSS58,
1912
2184
  parseTaoAmount,
2185
+ partnerFeeCut,
2186
+ partnerFeeTaoTopUp,
1913
2187
  sourceChainId,
1914
2188
  ss58ToPublicKey,
1915
2189
  toEip1193Transaction,