@xswap-link/sdk 0.12.2 → 0.13.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xswap-link/sdk",
3
- "version": "0.12.2",
3
+ "version": "0.13.1",
4
4
  "description": "JavaScript SDK for XSwap platform",
5
5
  "homepage": "https://github.com/xswap-link/xswap-sdk",
6
6
  "repository": {
@@ -11,11 +11,7 @@ import {
11
11
  crosschainEstimationTimes,
12
12
  } from "@src/constants";
13
13
  import { useHistory, useSwapContext, useTxUIWrapper } from "@src/context";
14
- import {
15
- ADDRESSES,
16
- CustomXSwapRouterAbi,
17
- XSwapRouterAbi,
18
- } from "@src/contracts";
14
+ import { ADDRESSES, XSwapRouterAbi } from "@src/contracts";
19
15
  import { useEvmContractApi } from "@src/hooks";
20
16
  import useNetworks from "@src/hooks/networkManagement/useNetworks";
21
17
  import {
@@ -131,11 +127,6 @@ export const TxOverview = ({ onCloseClick }: Props) => {
131
127
  "MessageSent(bytes32,uint64,address,bytes,address,uint256,uint256,address,uint256)"; // TODO think of getting this signature in a different way becase its easy to forgot about it when we will change event on smartcontract
132
128
  const messageReceivedEventSignature =
133
129
  "MessageReceived(bytes32,uint64,address,bytes,address,uint256)"; // TODO think of getting this signature in a different way because its easy to forgot about it when we will change event on smartcontract
134
- // cross-chain bridge via custom xswap router
135
- const messageSentXSwapCustomRouterEventSignature =
136
- "MessageSent(bytes32,uint64,address,address,address,address,uint256)"; // TODO think of getting this signature in a different way becase its easy to forgot about it when we will change event on smartcontract
137
- const messageReceivedXSwapCustomRouterEventSignature =
138
- "MessageReceived(bytes32,uint64,address,bytes)"; // TODO think of getting this signature in a different way because its easy to forgot about it when we will change event on smartcontract
139
130
 
140
131
  const destinationChainProvider = ethers.getDefaultProvider(
141
132
  supportedChains.find(
@@ -146,7 +137,6 @@ export const TxOverview = ({ onCloseClick }: Props) => {
146
137
  );
147
138
 
148
139
  const handleMessageEvent = async (
149
- contractKey: "XSwapRouter" | "CustomXSwapRouter",
150
140
  messageSentEventSignature: string,
151
141
  messageReceivedEventSignature: string,
152
142
  ) => {
@@ -159,15 +149,13 @@ export const TxOverview = ({ onCloseClick }: Props) => {
159
149
  transactionData.messageId = messageId;
160
150
 
161
151
  const xSwapRouterOnDestination = new ethers.Contract(
162
- ADDRESSES[dstChain!.chainId]![contractKey]!,
163
- contractKey === "XSwapRouter"
164
- ? XSwapRouterAbi
165
- : CustomXSwapRouterAbi,
152
+ ADDRESSES[dstChain!.chainId]!.XSwapRouter!,
153
+ XSwapRouterAbi,
166
154
  destinationChainProvider,
167
155
  );
168
156
 
169
157
  const eventFilter = {
170
- address: ADDRESSES[dstChain.chainId]?.[contractKey],
158
+ address: ADDRESSES[dstChain.chainId]?.XSwapRouter,
171
159
  topics: [
172
160
  ethers.utils.id(messageReceivedEventSignature),
173
161
  messageId,
@@ -194,16 +182,9 @@ export const TxOverview = ({ onCloseClick }: Props) => {
194
182
  };
195
183
 
196
184
  await handleMessageEvent(
197
- "XSwapRouter",
198
185
  messageSentEventSignature,
199
186
  messageReceivedEventSignature,
200
187
  );
201
-
202
- await handleMessageEvent(
203
- "CustomXSwapRouter",
204
- messageSentXSwapCustomRouterEventSignature,
205
- messageReceivedXSwapCustomRouterEventSignature,
206
- );
207
188
  }
208
189
 
209
190
  addTransactionToLocalHistory(transactionData, txReceipt.from);
@@ -241,7 +222,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
241
222
  handlers: {
242
223
  onSuccess: (data) => {
243
224
  setTxStatus(TxStatus.COMPLETED);
244
- refreshBalance();
225
+ refreshBalance({ forceRpc: true });
245
226
  trackTransaction(data);
246
227
  if (
247
228
  srcChain?.chainId &&
@@ -419,7 +400,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
419
400
  handlers: {
420
401
  onSuccess: () => {
421
402
  setTxStatus(TxStatus.COMPLETED);
422
- refreshBalance();
403
+ refreshBalance({ forceRpc: true });
423
404
  },
424
405
  },
425
406
  network: Ecosystem.SOLANA,
@@ -8,7 +8,7 @@ import {
8
8
  SOLANA_TOKEN_2022_PROGRAM_ID,
9
9
  SOLANA_TOKEN_PROGRAM_ID,
10
10
  } from "@src/constants";
11
- import { ADDRESSES } from "@src/contracts";
11
+ import { ADDRESSES, resolveBridgeDisplayToken } from "@src/contracts";
12
12
  import {
13
13
  BridgeToken,
14
14
  BridgeTokensDictionary,
@@ -117,7 +117,7 @@ type SwapContext = {
117
117
  isFetchingBalances: boolean;
118
118
  isExpressDeliveryActive: boolean;
119
119
  quoteRoute: () => Promise<void>;
120
- refreshBalance: () => void;
120
+ refreshBalance: (options?: { forceRpc?: boolean }) => void;
121
121
  findTokenDataByAddressAndChain: (
122
122
  tokenAddress: string,
123
123
  chainId: string,
@@ -279,6 +279,7 @@ export const SwapProvider = ({
279
279
 
280
280
  const routeAbortController = useRef<AbortController | null>(null);
281
281
  const refreshSelectedTokenBalanceNonce = useRef(crypto.randomUUID());
282
+ const prevWalletAddresses = useRef<string | undefined>(undefined);
282
283
 
283
284
  // =============================================================================
284
285
  // Memoized Values
@@ -383,8 +384,15 @@ export const SwapProvider = ({
383
384
  // Skip invalid token pairs.
384
385
  continue;
385
386
  }
386
- const [chainId0, address0] = entries[0]!;
387
- const [chainId1, address1] = entries[1]!;
387
+ const [chainId0, rawAddress0] = entries[0]!;
388
+ const [chainId1, rawAddress1] = entries[1]!;
389
+
390
+ // Some pairs use an internal, CCIP-connected token (e.g. a wrapped
391
+ // variant) that the backend relies on for routing. In the bridge UI we
392
+ // display/select the user-facing token instead; the backend wraps it into
393
+ // the CCIP token when building the route.
394
+ const address0 = resolveBridgeDisplayToken(chainId0, rawAddress0);
395
+ const address1 = resolveBridgeDisplayToken(chainId1, rawAddress1);
388
396
 
389
397
  addMapping(chainId0, address0, chainId1, address1);
390
398
  addMapping(chainId1, address1, chainId0, address0);
@@ -661,77 +669,96 @@ export const SwapProvider = ({
661
669
  srcValueWei,
662
670
  ]);
663
671
  // refreshes and updates the balance of the source token using its RPC URL.
664
- const refreshBalance = useCallback(async () => {
665
- if (
666
- (!evmAddress && !solanaAddress) ||
667
- !srcChain?.publicRpcUrls[0] ||
668
- !srcToken ||
669
- !srcToken?.address
670
- ) {
671
- return;
672
- }
673
-
674
- const nonce = crypto.randomUUID();
675
- refreshSelectedTokenBalanceNonce.current = nonce;
676
- setIsFetchingBalance(true);
677
- let balance = "0";
678
- if (srcChain?.ecosystem === Ecosystem.EVM && evmAddress) {
679
- const balanceFromBackend =
680
- tokenBalances[srcChain?.chainId]?.[srcToken.address];
681
- if (balanceFromBackend === undefined) {
682
- const balanceUI = await getBalanceOf(
683
- evmAddress,
684
- srcToken.address,
685
- srcChain?.publicRpcUrls[0],
686
- );
687
- balance = humanReadableToWei({
688
- amount: balanceUI,
689
- decimals: srcToken.decimals,
690
- });
691
- } else {
692
- balance = balanceFromBackend;
672
+ // pass { forceRpc: true } to skip the backend balances cache (e.g. right
673
+ // after a transaction, when the cached value is already stale).
674
+ const refreshBalance = useCallback(
675
+ async (options?: { forceRpc?: boolean }) => {
676
+ if (
677
+ (!evmAddress && !solanaAddress) ||
678
+ !srcChain?.publicRpcUrls[0] ||
679
+ !srcToken ||
680
+ !srcToken?.address
681
+ ) {
682
+ return;
693
683
  }
694
- } else if (srcChain?.ecosystem === Ecosystem.SOLANA && solanaAddress) {
695
- const solanaPublicKey = new PublicKey(solanaAddress);
696
- const connection = new Connection(srcChain.publicRpcUrls[0]!);
697
- if (srcToken.address === SOLANA_NATIVE_TOKEN_ADDRESS.toBase58()) {
698
- balance = new BigNumberJS(
699
- await connection.getBalance(solanaPublicKey),
700
- ).toString();
701
- } else {
702
- const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
703
- solanaPublicKey,
704
- {
705
- programId: SOLANA_TOKEN_PROGRAM_ID,
706
- },
707
- );
708
- const token2022Accounts =
709
- await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
710
- programId: SOLANA_TOKEN_2022_PROGRAM_ID,
684
+
685
+ const nonce = crypto.randomUUID();
686
+ refreshSelectedTokenBalanceNonce.current = nonce;
687
+ setIsFetchingBalance(true);
688
+ let balance = "0";
689
+ if (srcChain?.ecosystem === Ecosystem.EVM && evmAddress) {
690
+ const balanceFromBackend =
691
+ tokenBalances[srcChain?.chainId]?.[srcToken.address];
692
+ if (options?.forceRpc || balanceFromBackend === undefined) {
693
+ const balanceUI = await getBalanceOf(
694
+ evmAddress,
695
+ srcToken.address,
696
+ srcChain?.publicRpcUrls[0],
697
+ );
698
+ balance = humanReadableToWei({
699
+ amount: balanceUI,
700
+ decimals: srcToken.decimals,
711
701
  });
712
- const allTokenAccount = [
713
- ...tokenAccounts.value,
714
- ...token2022Accounts.value,
715
- ];
716
- const tokenAccount = allTokenAccount.find(
717
- (account) =>
718
- account.account.data.parsed.info.mint === srcToken.address,
719
- );
720
- if (!tokenAccount) {
721
- balance = "0";
702
+ if (options?.forceRpc && balanceFromBackend !== undefined) {
703
+ // keep the backend cache in sync so token pickers and future
704
+ // non-forced refreshes see the fresh value
705
+ const chainId = srcChain.chainId;
706
+ const tokenAddress = srcToken.address;
707
+ const freshBalance = balance;
708
+ setTokenBalances((prev) => ({
709
+ ...prev,
710
+ [chainId]: {
711
+ ...prev[chainId],
712
+ [tokenAddress]: freshBalance,
713
+ },
714
+ }));
715
+ }
722
716
  } else {
723
- balance = tokenAccount.account.data.parsed.info.tokenAmount.amount;
717
+ balance = balanceFromBackend;
718
+ }
719
+ } else if (srcChain?.ecosystem === Ecosystem.SOLANA && solanaAddress) {
720
+ const solanaPublicKey = new PublicKey(solanaAddress);
721
+ const connection = new Connection(srcChain.publicRpcUrls[0]!);
722
+ if (srcToken.address === SOLANA_NATIVE_TOKEN_ADDRESS.toBase58()) {
723
+ balance = new BigNumberJS(
724
+ await connection.getBalance(solanaPublicKey),
725
+ ).toString();
726
+ } else {
727
+ const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
728
+ solanaPublicKey,
729
+ {
730
+ programId: SOLANA_TOKEN_PROGRAM_ID,
731
+ },
732
+ );
733
+ const token2022Accounts =
734
+ await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
735
+ programId: SOLANA_TOKEN_2022_PROGRAM_ID,
736
+ });
737
+ const allTokenAccount = [
738
+ ...tokenAccounts.value,
739
+ ...token2022Accounts.value,
740
+ ];
741
+ const tokenAccount = allTokenAccount.find(
742
+ (account) =>
743
+ account.account.data.parsed.info.mint === srcToken.address,
744
+ );
745
+ if (!tokenAccount) {
746
+ balance = "0";
747
+ } else {
748
+ balance = tokenAccount.account.data.parsed.info.tokenAmount.amount;
749
+ }
724
750
  }
725
751
  }
726
- }
727
- if (nonce !== refreshSelectedTokenBalanceNonce.current) {
728
- return;
729
- }
752
+ if (nonce !== refreshSelectedTokenBalanceNonce.current) {
753
+ return;
754
+ }
730
755
 
731
- setSrcTokenBalanceWei(balance);
756
+ setSrcTokenBalanceWei(balance);
732
757
 
733
- setIsFetchingBalance(false);
734
- }, [evmAddress, solanaAddress, srcChain, srcToken, tokenBalances]);
758
+ setIsFetchingBalance(false);
759
+ },
760
+ [evmAddress, solanaAddress, srcChain, srcToken, tokenBalances],
761
+ );
735
762
  // retrieves token details for a given token address and chain from supported tokens mapping. (including imported tokens)
736
763
  const findTokenDataByAddressAndChain = useCallback(
737
764
  (tokenAddress: string, chainId: string): Token | undefined => {
@@ -794,9 +821,16 @@ export const SwapProvider = ({
794
821
  quoteRoute();
795
822
  }, [quoteRoute]);
796
823
  // calls refreshBalance to update source token balance on mount or when refreshBalance changes.
824
+ // when the connected wallet changed, force an RPC read — the cached backend
825
+ // balances still belong to the previous wallet until getBalances resolves.
797
826
  useEffect(() => {
798
- refreshBalance();
799
- }, [refreshBalance]);
827
+ const walletAddresses = `${evmAddress ?? ""}:${solanaAddress ?? ""}`;
828
+ const walletChanged =
829
+ prevWalletAddresses.current !== undefined &&
830
+ prevWalletAddresses.current !== walletAddresses;
831
+ prevWalletAddresses.current = walletAddresses;
832
+ refreshBalance(walletChanged ? { forceRpc: true } : undefined);
833
+ }, [refreshBalance, evmAddress, solanaAddress]);
800
834
  // fetches user all token balances when a wallet address is available and supported chains are loaded.
801
835
  useEffect(() => {
802
836
  let isCancelled = false;
@@ -2,12 +2,10 @@ import BatchQuery from "./BatchQuery.json";
2
2
  import ERC20 from "./ERC20.json";
3
3
  import XSwapRouter from "./XSwapRouter.json";
4
4
  import XSwapTokenFeeDistributor from "./XSwapTokenFeeDistributor.json";
5
- import CustomXSwapRouter from "./CustomXSwapRouter.json";
6
5
  import StakingStableAPR from "./StakingStableAPR.json";
7
6
 
8
7
  export const BatchQueryAbi = BatchQuery;
9
8
  export const ERC20Abi = ERC20;
10
9
  export const XSwapRouterAbi = XSwapRouter;
11
10
  export const XSwapTokenFeeDistributorAbi = XSwapTokenFeeDistributor;
12
- export const CustomXSwapRouterAbi = CustomXSwapRouter;
13
11
  export const StakingStableAPRAbi = StakingStableAPR;
@@ -23,8 +23,6 @@ export const ADDRESSES: Addresses = {
23
23
  "8453": {
24
24
  [ContractName.USDC]: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
25
25
  [ContractName.BatchQuery]: "0xb5f9934ea0810ef5faa677e8c68c6983b88664b1",
26
- [ContractName.CustomXSwapRouter]:
27
- "0x9B64aC0aD647768838a3957Bc12c40E1C539cb93",
28
26
  [ContractName.FeeCollector]: "0xb37275558f02f05104c2ba35199d16adeb43432f",
29
27
  [ContractName.StakingCosmicCadets]:
30
28
  "0x67798736bFb7d9A1F143215E258fe15Da68c85F0",
@@ -41,8 +39,6 @@ export const ADDRESSES: Addresses = {
41
39
  "1": {
42
40
  [ContractName.USDC]: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
43
41
  [ContractName.BatchQuery]: "0xb5f9934ea0810ef5faa677e8c68c6983b88664b1",
44
- [ContractName.CustomXSwapRouter]:
45
- "0x9B64aC0aD647768838a3957Bc12c40E1C539cb93",
46
42
  [ContractName.FeeCollector]: "0xb37275558f02f05104c2ba35199d16adeb43432f",
47
43
  [ContractName.XSwapRouter]: "0xe1c14b9f065dead2e89ee35382f8bd42bdb87a04",
48
44
  [ContractName.XSwapToken]: "0x8fe815417913a93ea99049fc0718ee1647a2a07c",
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Bridge UI display overrides.
3
+ *
4
+ * Some tokens returned by the `/bridgeTokens` endpoint are internal,
5
+ * directly-CCIP-connected tokens (e.g. a wrapped variant) that the backend
6
+ * relies on for routing, but that we do NOT want users to see or pick in the
7
+ * bridge UI. This map replaces such a CCIP token with the user-facing token we
8
+ * display and let users select / hold instead.
9
+ *
10
+ * The backend accepts the user-facing token in `/route` and wraps it into the
11
+ * CCIP token under the hood (e.g. real USDC -> XSwapUSDC via the wrapper), so
12
+ * the selected token is what flows through balances, approval and the route
13
+ * request, while the actual cross-chain hop still uses the CCIP token.
14
+ *
15
+ * Shape: { [chainId]: { [ccipTokenAddressLowercased]: displayTokenAddressLowercased } }
16
+ *
17
+ * IMPORTANT: addresses must be lowercased on both sides — the bridge dictionary
18
+ * and its consumers compare EVM addresses lowercased.
19
+ */
20
+ export const bridgeDisplayTokenOverrides: Record<
21
+ string,
22
+ Record<string, string>
23
+ > = {
24
+ // base: XSwapUSDC -> real USDC
25
+ "8453": {
26
+ "0x5426188a207fd0365dca5cff345059ff531d1aa7":
27
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
28
+ },
29
+ };
30
+
31
+ /**
32
+ * Returns the user-facing token address to display in the bridge UI for a given
33
+ * `/bridgeTokens` entry, or the original address when there is no override.
34
+ *
35
+ * EVM addresses are matched case-insensitively; non-EVM addresses (e.g. Solana)
36
+ * are returned untouched.
37
+ */
38
+ export const resolveBridgeDisplayToken = (
39
+ chainId: string,
40
+ tokenAddress: string,
41
+ ): string => {
42
+ return (
43
+ bridgeDisplayTokenOverrides[chainId]?.[tokenAddress.toLowerCase()] ??
44
+ tokenAddress
45
+ );
46
+ };
@@ -1,2 +1,3 @@
1
1
  export * from "./abi";
2
2
  export * from "./addresses";
3
+ export * from "./bridgeDisplayTokens";
@@ -9,7 +9,6 @@ export type Contracts = Partial<{
9
9
  export enum ContractName {
10
10
  USDC = "USDC",
11
11
  BatchQuery = "BatchQuery",
12
- CustomXSwapRouter = "CustomXSwapRouter",
13
12
  FeeCollector = "FeeCollector",
14
13
  StakingCosmicCadets = "StakingCosmicCadets",
15
14
  StakingSolarDivision = "StakingSolarDivision",