@xswap-link/sdk 0.9.6 → 0.9.7

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.
@@ -13,7 +13,6 @@ import {
13
13
  Token,
14
14
  TokenOption,
15
15
  TokenPrices,
16
- Transaction,
17
16
  Web3Environment,
18
17
  } from "@src/models";
19
18
  import {
@@ -27,11 +26,13 @@ import {
27
26
  getBalanceOf,
28
27
  humanReadableToWei,
29
28
  isServer,
29
+ resolveTokenAddress,
30
30
  safeBigNumberFrom,
31
+ sortChains,
31
32
  weiToHumanReadable,
32
33
  } from "@src/utils";
33
34
  import BigNumberJS from "bignumber.js";
34
- import { BigNumber, constants, ethers } from "ethers";
35
+ import { BigNumber, constants } from "ethers";
35
36
  import {
36
37
  createContext,
37
38
  Dispatch,
@@ -45,10 +46,14 @@ import {
45
46
  useState,
46
47
  } from "react";
47
48
  import { Config, useAccount } from "wagmi";
48
- import { useHistory } from "./HistoryProvider";
49
49
  import { mapToValidPath } from "@src/utils/validation";
50
50
 
51
51
  type Tab = "Swap" | "History" | "Settings";
52
+
53
+ type ChainOption = Chain & {
54
+ disabled: boolean;
55
+ };
56
+
52
57
  type SwapContext = {
53
58
  bridgeUI: boolean;
54
59
  integrationConfig: Partial<TxConfigFormPayload>;
@@ -64,6 +69,8 @@ type SwapContext = {
64
69
  setSrcChain: Dispatch<SetStateAction<Chain | undefined>>;
65
70
  srcChainTokensOptions: TokenOption[];
66
71
  srcChainOtherTokensOptions: TokenOption[];
72
+ srcChainOptions: ChainOption[];
73
+ dstChainOptions: ChainOption[];
67
74
  tokenPrices: TokenPrices;
68
75
  srcToken: Token | undefined;
69
76
  setSrcToken: Dispatch<SetStateAction<Token | undefined>>;
@@ -85,8 +92,6 @@ type SwapContext = {
85
92
  dstValueWei: string;
86
93
  dstValueUsd: string | null;
87
94
  dstValueMin: string;
88
- lastTx: Transaction | undefined;
89
- setLastTxHash: Dispatch<SetStateAction<string>>;
90
95
  expressDelivery: boolean;
91
96
  setExpressDelivery: Dispatch<SetStateAction<boolean>>;
92
97
  infiniteApproval: boolean;
@@ -101,6 +106,7 @@ type SwapContext = {
101
106
  srcTokenBalanceInfo: string;
102
107
  isFetchingRoute: boolean;
103
108
  isFetchingBalance: boolean;
109
+ isFetchingBalances: boolean;
104
110
  isExpressDeliveryActive: boolean;
105
111
  quoteRoute: () => Promise<void>;
106
112
  refreshBalance: () => void;
@@ -146,7 +152,6 @@ const init: SwapContext = {
146
152
  dstValueWei: "",
147
153
  dstValueUsd: null,
148
154
  dstValueMin: "",
149
- lastTx: undefined,
150
155
  expressDelivery: true,
151
156
  infiniteApproval: true,
152
157
  slippage: "1.5",
@@ -155,6 +160,7 @@ const init: SwapContext = {
155
160
  srcTokenBalanceWei: "",
156
161
  srcTokenBalanceInfo: "-",
157
162
  isFetchingBalance: false,
163
+ isFetchingBalances: false,
158
164
  isFetchingRoute: false,
159
165
  isExpressDeliveryActive: true,
160
166
  setTab: functionNotImplemented,
@@ -164,7 +170,6 @@ const init: SwapContext = {
164
170
  setSrcToken: functionNotImplemented,
165
171
  setDstToken: functionNotImplemented,
166
172
  setSrcValue: functionNotImplemented,
167
- setLastTxHash: functionNotImplemented,
168
173
  setExpressDelivery: functionNotImplemented,
169
174
  setInfiniteApproval: functionNotImplemented,
170
175
  setSlippage: functionNotImplemented,
@@ -176,6 +181,8 @@ const init: SwapContext = {
176
181
  bridgeTokensDictionary: null,
177
182
  onConnectWallet: functionNotImplemented,
178
183
  hasOnConnectWalletCallback: false,
184
+ srcChainOptions: [],
185
+ dstChainOptions: [],
179
186
  };
180
187
 
181
188
  const swapContext = createContext(init);
@@ -194,61 +201,47 @@ export const SwapProvider = ({
194
201
  wagmiConfig,
195
202
  overlay,
196
203
  }: Props) => {
204
+ // =============================================================================
205
+ // Hooks
206
+ // =============================================================================
197
207
  const { address, chainId: walletChainId } = useAccount();
208
+
209
+ // =============================================================================
210
+ // State
211
+ // =============================================================================
212
+ const [isInitialStateSet, setIsInitialStateSet] = useState(false);
213
+ // Settings
198
214
  const [tab, setTab] = useState(init.tab);
199
215
  const [error, setError] = useState(init.error);
200
216
  const [expressDelivery, setExpressDelivery] = useState(init.expressDelivery);
217
+ const [slippage, setSlippage] = useState(init.slippage);
201
218
  const [infiniteApproval, setInfiniteApproval] = useState(
202
219
  init.infiniteApproval,
203
220
  );
204
- const srcTokenLocked = useMemo(
205
- () => !!integrationConfig?.srcTokenLocked,
206
- [integrationConfig?.srcTokenLocked],
207
- );
208
- const dstTokenLocked = useMemo(
209
- () => !!integrationConfig?.dstTokenLocked,
210
- [integrationConfig?.dstTokenLocked],
211
- );
212
- const srcChainLocked = useMemo(
213
- () => !!integrationConfig?.srcChainLocked,
214
- [integrationConfig?.srcChainLocked],
215
- );
216
- const dstChainLocked = useMemo(
217
- () => !!integrationConfig?.dstChainLocked,
218
- [integrationConfig?.dstChainLocked],
219
- );
220
-
221
- const bridgeUI = useMemo(
222
- () => !!integrationConfig.bridge,
223
- [integrationConfig.bridge],
224
- );
225
-
226
- const [slippage, setSlippage] = useState(init.slippage);
221
+ // Chains
227
222
  const [srcChain, setSrcChain] = useState(init.srcChain);
228
223
  const [dstChain, setDstChain] = useState(init.dstChain);
229
224
  const [feeToken, setFeeToken] = useState(init.feeToken);
225
+ // Tokens
230
226
  const [srcToken, setSrcToken] = useState(init.srcToken);
231
227
  const [dstToken, setDstToken] = useState(init.dstToken);
228
+ const [tokenPrices, setTokenPrices] = useState(init.tokenPrices);
229
+ const [tokenBalances, setTokenBalances] = useState<
230
+ Record<string, Record<string, string>>
231
+ >({});
232
+ const [bridgeTokens, setBridgeTokens] = useState<BridgeToken[]>([]);
233
+ // Values
232
234
  const [srcValue, setSrcValue] = useState(init.srcValue);
233
-
234
- const [isFetchingBalance, setIsFetchingBalance] = useState(
235
- init.isFetchingBalance,
236
- );
237
235
  const [srcTokenBalanceWei, setSrcTokenBalanceWei] = useState(
238
236
  init.srcTokenBalanceWei,
239
237
  );
240
- const [isFetchingRoute, setFetchingRoute] = useState(init.isFetchingRoute);
241
- const [route, setRoute] = useState(init.route);
242
-
243
- const [lastTxHash, setLastTxHash] = useState("");
244
-
238
+ // Chains and Token options for pickers
245
239
  const [srcChainTokensOptions, setSrcChainTokensOptions] = useState(
246
240
  init.srcChainTokensOptions,
247
241
  );
248
242
  const [dstChainTokensOptions, setDstChainTokensOptions] = useState(
249
243
  init.dstChainTokensOptions,
250
244
  );
251
- const [tokenPrices, setTokenPrices] = useState(init.tokenPrices);
252
245
  const [srcChainOtherTokensOptions, setSrcChainOtherTokensOptions] = useState(
253
246
  init.srcChainOtherTokensOptions,
254
247
  );
@@ -256,24 +249,62 @@ export const SwapProvider = ({
256
249
  init.dstChainOtherTokensOptions,
257
250
  );
258
251
 
252
+ const [srcChainOptions, setSrcChainOptions] = useState<ChainOption[]>([]);
253
+ const [dstChainOptions, setDstChainOptions] = useState<ChainOption[]>([]);
254
+ // Loaders
255
+ const [isFetchingBalance, setIsFetchingBalance] = useState(
256
+ init.isFetchingBalance,
257
+ );
258
+ const [isFetchingBalances, setIsFetchingBalances] = useState(
259
+ init.isFetchingBalances,
260
+ );
261
+ const [isFetchingRoute, setFetchingRoute] = useState(init.isFetchingRoute);
262
+ // Route and tx data
263
+ const [route, setRoute] = useState(init.route);
264
+
265
+ // =============================================================================
266
+ // Refs
267
+ // =============================================================================
268
+
259
269
  const routeAbortController = useRef<AbortController | null>(null);
260
270
  const refreshSelectedTokenBalanceNonce = useRef(crypto.randomUUID());
261
271
 
262
- const { history } = useHistory();
272
+ // =============================================================================
273
+ // Memoized Values
274
+ // =============================================================================
263
275
 
264
- const [tokenBalances, setTokenBalances] = useState<
265
- Record<string, Record<string, string>>
266
- >({});
276
+ // useMemo: determines if the source token is locked based on integration config.
277
+ const srcTokenLocked = useMemo(
278
+ () => !!integrationConfig?.srcTokenLocked,
279
+ [integrationConfig?.srcTokenLocked],
280
+ );
281
+ // useMemo: determines if the destination token is locked based on integration config.
282
+ const dstTokenLocked = useMemo(
283
+ () => !!integrationConfig?.dstTokenLocked,
284
+ [integrationConfig?.dstTokenLocked],
285
+ );
286
+ // useMemo: determines if the destination chain is locked from integration config.
287
+ const dstChainLocked = useMemo(
288
+ () => !!integrationConfig?.dstChainLocked,
289
+ [integrationConfig?.dstChainLocked],
290
+ );
291
+ // useMemo: determines if the source chain is locked from integration config.
292
+ const srcChainLocked = useMemo(
293
+ () => !!integrationConfig?.srcChainLocked,
294
+ [integrationConfig?.srcChainLocked],
295
+ );
296
+ // useMemo: sets a flag to indicate if the bridge UI is enabled based on integration config.
297
+ const bridgeUI = useMemo(
298
+ () => !!integrationConfig.bridge,
299
+ [integrationConfig.bridge],
300
+ );
267
301
 
268
- useEffect(() => {
269
- if (supportedChains.length > 0) {
270
- const chainIds = supportedChains.map((chain) => chain.chainId);
271
- getPrices({ chainId: chainIds }).then((prices) => {
272
- setTokenPrices(prices);
273
- });
274
- }
275
- }, [supportedChains]);
302
+ // useMemo: determines if a wallet connection callback exists.
303
+ const hasOnConnectWalletCallback = useMemo(() => {
304
+ return !!(integrationConfig.onConnectWallet || window.xPayOnConnectWallet);
305
+ }, [integrationConfig.onConnectWallet]);
276
306
 
307
+ // useMemo: merges supported chain tokens with custom tokens from localStorage.
277
308
  const supportedTokens: Record<string, Record<string, Token>> = useMemo(() => {
278
309
  const customTokens = !isServer
279
310
  ? JSON.parse(localStorage.getItem("custom-tokens") || "{}")
@@ -287,14 +318,14 @@ export const SwapProvider = ({
287
318
  supportedChains.forEach((chain) => {
288
319
  chainIdToTokenAddressToData[chain.chainId] = {};
289
320
  chain.tokens.forEach((token) => {
290
- // @ts-expect-error TODO
291
- chainIdToTokenAddressToData[chain.chainId][token.address] = token;
321
+ chainIdToTokenAddressToData[chain.chainId]![token.address] = token;
292
322
  });
293
323
  });
294
324
  return deepMergeObjects(chainIdToTokenAddressToData, customTokens);
295
325
  // eslint-disable-next-line react-hooks/exhaustive-deps
296
326
  }, [supportedChains]);
297
327
 
328
+ // useMemo: creates a mapping from each chain ID to available token options sorted by token priority.
298
329
  const chainToTokenOptionsMap = useMemo(() => {
299
330
  const tempChainToTokenOptionsMap: Record<string, TokenOption[]> = {};
300
331
  Object.entries(supportedTokens).forEach(([chainId, chainData]) => {
@@ -313,108 +344,43 @@ export const SwapProvider = ({
313
344
  return tempChainToTokenOptionsMap;
314
345
  }, [supportedTokens]);
315
346
 
316
- const [bridgeTokens, setBridgeTokens] = useState<BridgeToken[]>([]);
317
- useEffect(() => {
318
- (async () => {
319
- const data = await getBridgeTokens();
320
- setBridgeTokens(data);
321
- })();
322
- }, []);
323
-
347
+ // useMemo: constructs a dictionary mapping tokens across chains for bridging based on bridgeTokens data.
324
348
  const bridgeTokensDictionary = useMemo(() => {
325
- const result: BridgeTokensDictionary = {};
326
-
327
- for (const dict of bridgeTokens) {
328
- const [chainId0, address0] = Object.entries(dict)[0]!;
329
- const [chainId1, address1] = Object.entries(dict)[1]!;
330
- if (!result[chainId0]) {
331
- result[chainId0] = {};
332
- }
333
- if (!result[chainId0][address0]) {
334
- result[chainId0][address0] = {};
349
+ // If there are no bridge tokens, return null directly.
350
+ if (!bridgeTokens || bridgeTokens.length === 0) return null;
351
+
352
+ const dictionary: BridgeTokensDictionary = {};
353
+ // Helper to add a mapping from one token to its counterpart.
354
+ const addMapping = (
355
+ chainIdFrom: string,
356
+ tokenAddressFrom: string,
357
+ chainIdTo: string,
358
+ tokenAddressTo: string,
359
+ ) => {
360
+ if (!dictionary[chainIdFrom]) {
361
+ dictionary[chainIdFrom] = {};
335
362
  }
336
- result[chainId0][address0][chainId1] = address1;
337
- if (!result[chainId1]) {
338
- result[chainId1] = {};
363
+ if (!dictionary[chainIdFrom][tokenAddressFrom]) {
364
+ dictionary[chainIdFrom][tokenAddressFrom] = {};
339
365
  }
340
- if (!result[chainId1][address1]) {
341
- result[chainId1][address1] = {};
366
+ dictionary[chainIdFrom][tokenAddressFrom][chainIdTo] = tokenAddressTo;
367
+ };
368
+ // Process each token pair from bridgeTokens.
369
+ for (const tokenPair of bridgeTokens) {
370
+ const entries = Object.entries(tokenPair);
371
+ if (entries.length !== 2) {
372
+ // Skip invalid token pairs.
373
+ continue;
342
374
  }
343
- result[chainId1][address1][chainId0] = address0;
344
- }
375
+ const [chainId0, address0] = entries[0]!;
376
+ const [chainId1, address1] = entries[1]!;
345
377
 
346
- if (Object.keys(result).length) {
347
- return result;
378
+ addMapping(chainId0, address0, chainId1, address1);
379
+ addMapping(chainId1, address1, chainId0, address0);
348
380
  }
349
-
350
- return null;
381
+ return Object.keys(dictionary).length > 0 ? dictionary : null;
351
382
  }, [bridgeTokens]);
352
-
353
- const getTokenOptions = useCallback(
354
- (chainId: string | undefined) => {
355
- if (!chainId) {
356
- return [];
357
- }
358
-
359
- if (bridgeTokensDictionary && bridgeUI) {
360
- if (!chainToTokenOptionsMap[chainId]) {
361
- return [];
362
- }
363
-
364
- return chainToTokenOptionsMap[chainId].filter((tokenData) => {
365
- const supportedTokensAddresses = Object.keys(
366
- bridgeTokensDictionary[chainId] || {},
367
- );
368
-
369
- return supportedTokensAddresses.includes(tokenData.address);
370
- });
371
- }
372
-
373
- return chainToTokenOptionsMap[chainId] || [];
374
- },
375
- [bridgeTokensDictionary, bridgeUI, chainToTokenOptionsMap],
376
- );
377
-
378
- const srcTokenBalanceInfo = useMemo(() => {
379
- if (!srcToken || !srcTokenBalanceWei || !address) {
380
- return "";
381
- }
382
-
383
- if (safeBigNumberFrom(srcTokenBalanceWei).eq(0)) {
384
- return "0";
385
- }
386
-
387
- const fullHumanReadableBalance = weiToHumanReadable({
388
- amount: srcTokenBalanceWei,
389
- decimals: srcToken.decimals,
390
- precisionFractionalPlaces: srcToken.decimals,
391
- });
392
-
393
- if (new BigNumberJS(fullHumanReadableBalance).lt("0.00001")) {
394
- return "<0.00001";
395
- }
396
-
397
- // Shortened to cut non-significant decimals
398
- return weiToHumanReadable({
399
- amount: srcTokenBalanceWei,
400
- decimals: srcToken.decimals,
401
- precisionFractionalPlaces: 5,
402
- });
403
- }, [srcToken, srcTokenBalanceWei]);
404
-
405
- const lastTx = useMemo(() => {
406
- return history?.find(({ hash }) => hash === lastTxHash);
407
- }, [history, lastTxHash]);
408
-
409
- const srcValueWei = useMemo(
410
- () =>
411
- humanReadableToWei({
412
- amount: srcValue,
413
- decimals: srcToken?.decimals || 18,
414
- }),
415
- [srcToken?.decimals, srcValue],
416
- );
417
-
383
+ // useMemo: calculates the USD value for the source token amount using the current token prices.
418
384
  const srcValueUsd = useMemo(() => {
419
385
  if (
420
386
  srcToken &&
@@ -430,6 +396,16 @@ export const SwapProvider = ({
430
396
  }
431
397
  return null;
432
398
  }, [tokenPrices, srcToken, srcValue, srcChain]);
399
+ // useMemo: converts the source token value from human-readable format to Wei.
400
+ const srcValueWei = useMemo(
401
+ () =>
402
+ humanReadableToWei({
403
+ amount: srcValue,
404
+ decimals: srcToken?.decimals || 18,
405
+ }),
406
+ [srcToken?.decimals, srcValue],
407
+ );
408
+ // useMemo: converts the estimated output amount from the route to a human-readable destination token value.
433
409
  const dstValue = useMemo(
434
410
  () =>
435
411
  weiToHumanReadable({
@@ -440,6 +416,7 @@ export const SwapProvider = ({
440
416
  // eslint-disable-next-line react-hooks/exhaustive-deps
441
417
  [route],
442
418
  );
419
+ // useMemo: converts the human-readable destination value back to Wei using the destination token's decimals.
443
420
  const dstValueWei = useMemo(
444
421
  () =>
445
422
  humanReadableToWei({
@@ -448,6 +425,7 @@ export const SwapProvider = ({
448
425
  }),
449
426
  [dstToken?.decimals, dstValue],
450
427
  );
428
+ // useMemo: calculates the USD value for the destination token amount using the token prices.
451
429
  const dstValueUsd = useMemo(() => {
452
430
  if (
453
431
  dstToken &&
@@ -463,6 +441,7 @@ export const SwapProvider = ({
463
441
  }
464
442
  return null;
465
443
  }, [tokenPrices, dstToken, dstValue, dstChain]);
444
+ // useMemo: computes the minimum destination token value in human-readable format from route data.
466
445
  const dstValueMin = useMemo(
467
446
  () =>
468
447
  weiToHumanReadable({
@@ -473,7 +452,7 @@ export const SwapProvider = ({
473
452
  // eslint-disable-next-line react-hooks/exhaustive-deps
474
453
  [route],
475
454
  );
476
-
455
+ // useMemo: checks if express delivery is enabled and if the route includes a non-zero express delivery fee.
477
456
  const isExpressDeliveryActive = useMemo(
478
457
  () =>
479
458
  expressDelivery &&
@@ -481,12 +460,72 @@ export const SwapProvider = ({
481
460
  BigNumber.from(route.xSwapFees?.expressDeliveryFee || "0").gt("0"),
482
461
  [expressDelivery, route],
483
462
  );
484
-
463
+ // useMemo: indicates whether asynchronous data is loaded based on the presence of supported chains.
485
464
  const isAsyncDataLoaded = useMemo(
486
- () => supportedChains.length > 0,
487
- [supportedChains],
465
+ () => supportedChains.length > 0 && bridgeTokens.length > 0,
466
+ [supportedChains, bridgeTokens],
488
467
  );
468
+ // useMemo: computes a formatted balance string for the source token based on its balance in Wei.
469
+ const srcTokenBalanceInfo = useMemo(() => {
470
+ if (!srcToken || !srcTokenBalanceWei || !address) {
471
+ return "";
472
+ }
473
+
474
+ if (safeBigNumberFrom(srcTokenBalanceWei).eq(0)) {
475
+ return "0";
476
+ }
477
+
478
+ const fullHumanReadableBalance = weiToHumanReadable({
479
+ amount: srcTokenBalanceWei,
480
+ decimals: srcToken.decimals,
481
+ precisionFractionalPlaces: srcToken.decimals,
482
+ });
483
+
484
+ if (new BigNumberJS(fullHumanReadableBalance).lt("0.00001")) {
485
+ return "<0.00001";
486
+ }
489
487
 
488
+ // Shortened to cut non-significant decimals
489
+ return weiToHumanReadable({
490
+ amount: srcTokenBalanceWei,
491
+ decimals: srcToken.decimals,
492
+ precisionFractionalPlaces: 5,
493
+ });
494
+ }, [srcToken, srcTokenBalanceWei]);
495
+
496
+ // =============================================================================
497
+ // Callbacks
498
+ // =============================================================================
499
+
500
+ // returns available token options for a given chain, filtering by bridge support if bridge UI is enabled.
501
+ // NOTE: This function does not handle any of the filtering
502
+ const getTokenOptions = useCallback(
503
+ (chainId: string | undefined) => {
504
+ if (!chainId) {
505
+ return [];
506
+ }
507
+
508
+ // swap
509
+ if (!bridgeUI) {
510
+ return chainToTokenOptionsMap[chainId] || [];
511
+ }
512
+
513
+ // bridge
514
+ if (!chainToTokenOptionsMap[chainId] || !bridgeTokensDictionary) {
515
+ return [];
516
+ }
517
+
518
+ const supportedTokensAddresses = Object.keys(
519
+ bridgeTokensDictionary[chainId] || {},
520
+ );
521
+
522
+ return chainToTokenOptionsMap[chainId].filter((tokenData) =>
523
+ supportedTokensAddresses.includes(tokenData.address),
524
+ );
525
+ },
526
+ [bridgeTokensDictionary, bridgeUI, chainToTokenOptionsMap],
527
+ );
528
+ // fetches and sets the route quote using the current input parameters, aborting any previous request.
490
529
  const quoteRoute = useCallback(async () => {
491
530
  if (
492
531
  !srcChain ||
@@ -550,59 +589,139 @@ export const SwapProvider = ({
550
589
  srcToken,
551
590
  srcValueWei,
552
591
  ]);
592
+ // refreshes and updates the balance of the source token using its RPC URL.
593
+ const refreshBalance = useCallback(async () => {
594
+ if (
595
+ !address ||
596
+ !srcChain?.publicRpcUrls[0] ||
597
+ !srcToken ||
598
+ !srcToken?.address
599
+ ) {
600
+ return;
601
+ }
602
+
603
+ const nonce = crypto.randomUUID();
604
+ refreshSelectedTokenBalanceNonce.current = nonce;
605
+
606
+ setIsFetchingBalance(true);
607
+
608
+ const balance = await getBalanceOf(
609
+ address,
610
+ srcToken.address,
611
+ srcChain?.publicRpcUrls[0],
612
+ );
553
613
 
614
+ if (nonce !== refreshSelectedTokenBalanceNonce.current) {
615
+ return;
616
+ }
617
+
618
+ setSrcTokenBalanceWei(
619
+ humanReadableToWei({
620
+ amount: balance,
621
+ decimals: srcToken.decimals,
622
+ }),
623
+ );
624
+
625
+ setIsFetchingBalance(false);
626
+ }, [address, srcChain, srcToken]);
627
+ // retrieves token details for a given token address and chain from supported tokens mapping. (including imported tokens)
628
+ const findTokenDataByAddressAndChain = useCallback(
629
+ (tokenAddress: string, chainId: string): Token | undefined => {
630
+ return supportedTokens[chainId]?.[tokenAddress];
631
+ },
632
+ [supportedTokens],
633
+ );
634
+ // triggers the wallet connection process.
635
+ const onConnectWallet = useCallback(() => {
636
+ if (integrationConfig.onConnectWallet) {
637
+ integrationConfig.onConnectWallet();
638
+ } else if (window.xPayOnConnectWallet) {
639
+ window.xPayOnConnectWallet();
640
+ }
641
+ }, [integrationConfig]);
642
+
643
+ // =============================================================================
644
+ // Effects
645
+ // =============================================================================
646
+
647
+ // fetches token prices for supported chains once they are loaded.
648
+ useEffect(() => {
649
+ if (supportedChains.length > 0) {
650
+ const chainIds = supportedChains.map((chain) => chain.chainId);
651
+ getPrices({ chainId: chainIds }).then((prices) => {
652
+ setTokenPrices(prices);
653
+ });
654
+ }
655
+ }, [supportedChains]);
656
+ // fetches and sets bridge tokens on component mount.
657
+ useEffect(() => {
658
+ (async () => {
659
+ const data = await getBridgeTokens();
660
+ setBridgeTokens(data);
661
+ })();
662
+ }, []);
663
+ // recalculates the route quote when its dependency changes.
554
664
  useEffect(() => {
555
665
  quoteRoute();
556
666
  }, [quoteRoute]);
557
-
667
+ // calls refreshBalance to update source token balance on mount or when refreshBalance changes.
668
+ useEffect(() => {
669
+ refreshBalance();
670
+ }, [refreshBalance]);
671
+ // fetches user all token balances when a wallet address is available and supported chains are loaded.
558
672
  useEffect(() => {
559
673
  if (address && supportedChains.length > 0) {
674
+ setIsFetchingBalances(true);
560
675
  getBalances({ walletAddress: address }).then((balances) => {
561
676
  setTokenBalances(balances);
677
+ setIsFetchingBalances(false);
562
678
  });
563
679
  }
564
680
  }, [address, supportedChains.length]);
681
+ // sets the fee token from the source chain's tokens by selecting the native token (AddressZero).
682
+ useEffect(() => {
683
+ setFeeToken(
684
+ srcChain?.tokens.find(
685
+ ({ address }) =>
686
+ address.toLowerCase() === constants.AddressZero.toLowerCase(),
687
+ ),
688
+ );
689
+ }, [srcChain]);
565
690
 
691
+ // =============================================================================
692
+ // useEffects responsible for providing token and chain options
693
+ // =============================================================================
694
+
695
+ // updates token options for source and destination chains and other networks, incorporating balance info.
566
696
  useEffect(() => {
567
697
  (async () => {
568
- // source chain
698
+ const isSingleChainSelected = dstChain?.chainId === srcChain?.chainId;
699
+ // source chain options
569
700
  const srcTokenOptions: TokenOption[] = getTokenOptions(
570
701
  srcChain?.chainId,
571
702
  ).filter(({ symbol }) =>
572
- dstChain?.chainId === srcChain?.chainId
573
- ? symbol !== dstToken?.symbol
574
- : true,
703
+ isSingleChainSelected ? symbol !== dstToken?.symbol : true,
575
704
  );
576
705
  setSrcChainTokensOptions(srcTokenOptions);
577
-
578
- // destination chain
706
+ // destination chain options
579
707
  const dstTokenOptions: TokenOption[] = getTokenOptions(
580
708
  dstChain?.chainId,
581
709
  ).filter(({ symbol }) =>
582
- dstChain?.chainId === srcChain?.chainId
583
- ? symbol !== srcToken?.symbol
584
- : true,
710
+ isSingleChainSelected ? symbol !== srcToken?.symbol : true,
585
711
  );
586
712
  setDstChainTokensOptions(dstTokenOptions);
587
-
588
- // other source networks
713
+ // token options from other source networks
589
714
  const srcOtherTokenOptions: TokenOption[] = [];
590
715
  const srcOtherChains = supportedChains.filter(
591
- ({ chainId, web3Environment, swapSupported, bridgeSupported }) => {
592
- if (
593
- chainId === srcChain?.chainId ||
594
- web3Environment !== Web3Environment.MAINNET
595
- ) {
596
- return false;
597
- }
598
-
599
- if (bridgeUI) {
600
- return bridgeSupported;
601
- } else {
602
- return swapSupported;
603
- }
604
- },
716
+ ({ chainId, web3Environment, swapSupported, bridgeSupported }) =>
717
+ // same chain
718
+ chainId !== srcChain?.chainId &&
719
+ // not mainnet
720
+ web3Environment === Web3Environment.MAINNET &&
721
+ // bridgeUI and supported for bridge or !bridgeUI and supported for swap
722
+ ((bridgeUI && bridgeSupported) || (!bridgeUI && swapSupported)),
605
723
  );
724
+ // create token options from other source networks and filter out the destination token
606
725
  for (const { chainId } of srcOtherChains) {
607
726
  srcOtherTokenOptions.push(
608
727
  ...getTokenOptions(chainId).filter(({ symbol }) =>
@@ -611,15 +730,22 @@ export const SwapProvider = ({
611
730
  );
612
731
  }
613
732
  setSrcChainOtherTokensOptions(srcOtherTokenOptions);
614
-
615
- // other destination networks
733
+ // token options from other destination networks
616
734
  const dstOtherTokenOptions: TokenOption[] = [];
617
735
  const dstOtherChains = supportedChains.filter(
618
- ({ chainId, web3Environment, swapSupported }) =>
736
+ ({ chainId, web3Environment, swapSupported, bridgeSupported }) =>
619
737
  chainId !== dstChain?.chainId &&
738
+ // not mainnet
620
739
  web3Environment === Web3Environment.MAINNET &&
621
- swapSupported,
740
+ // bridgeUI and supported for bridge as destination or !bridgeUI and supported for swap as destination
741
+ ((bridgeUI &&
742
+ bridgeSupported &&
743
+ srcChain?.supportedDstForBridge?.includes(chainId)) ||
744
+ (!bridgeUI &&
745
+ swapSupported &&
746
+ srcChain?.supportedDstForSwap?.includes(chainId))),
622
747
  );
748
+ // create token options from other destination networks and filter out the source token
623
749
  for (const { chainId } of dstOtherChains) {
624
750
  dstOtherTokenOptions.push(
625
751
  ...getTokenOptions(chainId).filter(({ symbol }) =>
@@ -628,7 +754,7 @@ export const SwapProvider = ({
628
754
  );
629
755
  }
630
756
  setDstChainOtherTokensOptions(dstOtherTokenOptions);
631
-
757
+ // add balances to the token options
632
758
  const addBalances = (tokenOptions: TokenOption[]) => {
633
759
  return tokenOptions.map((token) => ({
634
760
  ...token,
@@ -637,7 +763,6 @@ export const SwapProvider = ({
637
763
  ),
638
764
  }));
639
765
  };
640
-
641
766
  setSrcChainTokensOptions(addBalances(srcTokenOptions));
642
767
  setDstChainTokensOptions(addBalances(dstTokenOptions));
643
768
  setSrcChainOtherTokensOptions(addBalances(srcOtherTokenOptions));
@@ -654,68 +779,108 @@ export const SwapProvider = ({
654
779
  tokenBalances,
655
780
  ]);
656
781
 
657
- const refreshBalance = useCallback(async () => {
658
- if (!address || !srcChain?.publicRpcUrls[0] || !srcToken) {
659
- return;
660
- }
661
-
662
- const nonce = crypto.randomUUID();
663
- refreshSelectedTokenBalanceNonce.current = nonce;
782
+ // updates the source and destination chain options based on network support and bridge configurations.
783
+ useEffect(() => {
784
+ const baseChains = supportedChains.filter(
785
+ ({ web3Environment, swapSupported, bridgeSupported }) =>
786
+ web3Environment === Web3Environment.MAINNET &&
787
+ ((bridgeUI && bridgeSupported) || (!bridgeUI && swapSupported)),
788
+ );
789
+ // Determine disabled source chains for bridge UI. For swap, the source chains are all the supported chains.
790
+ let sourceChainsDisabled: string[] = [];
791
+ // If we block any bridge source chain, we need to have dstChain and dstToken selected, otherwise we allow to pick any source chain.
792
+ if (bridgeUI && dstChain && dstToken && bridgeTokensDictionary) {
793
+ // Find chains that have tokens available for the selected destination chain
794
+ const chainsWithTokensForDestination = baseChains.filter(
795
+ ({ chainId }) => {
796
+ const bridgeTokens = bridgeTokensDictionary[chainId] || {};
797
+ return Object.values(bridgeTokens).some(
798
+ (targetChains) => targetChains[dstChain.chainId],
799
+ );
800
+ },
801
+ );
664
802
 
665
- setIsFetchingBalance(true);
803
+ // Disable chains that don't have tokens available for the destination
804
+ sourceChainsDisabled = baseChains
805
+ .filter(
806
+ (chain) =>
807
+ !chainsWithTokensForDestination.some(
808
+ (availableChain) => availableChain.chainId === chain.chainId,
809
+ ),
810
+ )
811
+ .map((chain) => chain.chainId);
812
+ }
666
813
 
667
- const balance = await getBalanceOf(
668
- address,
669
- srcToken.address,
670
- srcChain?.publicRpcUrls[0],
671
- );
814
+ // Determine disabled destination chains.
815
+ const destinationChainsDisabled = new Set<string>();
672
816
 
673
- if (nonce !== refreshSelectedTokenBalanceNonce.current) {
674
- return;
817
+ // If there is a source chain, we need to disable the destination chains that are not supported for the source chain.
818
+ if (srcChain) {
819
+ baseChains
820
+ .filter((chain) =>
821
+ bridgeUI
822
+ ? !srcChain.supportedDstForBridge?.includes(chain.chainId)
823
+ : !srcChain.supportedDstForSwap?.includes(chain.chainId),
824
+ )
825
+ .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
675
826
  }
676
827
 
677
- setSrcTokenBalanceWei(
678
- humanReadableToWei({
679
- amount: balance,
680
- decimals: srcToken.decimals,
681
- }),
682
- );
828
+ // If there is no source token, we need to disable the chains without any supported tokens.
829
+ if (bridgeUI && srcChain && bridgeTokensDictionary && !srcToken) {
830
+ const srcChainTokens = bridgeTokensDictionary[srcChain.chainId];
831
+ if (!srcChainTokens) {
832
+ // If source chain has no bridge tokens, disable all destination chains
683
833
 
684
- setIsFetchingBalance(false);
685
- }, [address, srcChain, srcToken]);
834
+ baseChains.forEach((chain) =>
835
+ destinationChainsDisabled.add(chain.chainId),
836
+ );
837
+ } else {
838
+ // Disable chains that have no tokens available from the source chain
839
+ baseChains
840
+ .filter((chain) => {
841
+ // Check if any token on source chain can bridge to this destination chain
842
+ return !Object.values(srcChainTokens).some(
843
+ (targetChains) => targetChains[chain.chainId],
844
+ );
845
+ })
846
+ .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
847
+ }
848
+ }
686
849
 
687
- const findTokenDataByAddressAndChain = useCallback(
688
- (tokenAddress: string, chainId: string): Token | undefined => {
689
- return supportedTokens[chainId]?.[tokenAddress];
690
- },
691
- [supportedTokens],
692
- );
850
+ // If there is a source token, we need to disable the chains that don't have tokens available for the source token.
851
+ if (bridgeUI && srcChain && bridgeTokensDictionary && srcToken) {
852
+ const availableChains =
853
+ bridgeTokensDictionary[srcChain.chainId]?.[srcToken.address];
854
+ baseChains
693
855
 
694
- useEffect(() => {
695
- refreshBalance();
696
- }, [refreshBalance]);
856
+ .filter((chain) => !availableChains?.[chain.chainId])
857
+ .forEach((chain) => destinationChainsDisabled.add(chain.chainId));
858
+ }
697
859
 
698
- useEffect(() => {
699
- setFeeToken(
700
- srcChain?.tokens.find(
701
- ({ address }) =>
702
- address.toLowerCase() === constants.AddressZero.toLowerCase(),
703
- ),
704
- );
705
- }, [srcChain]);
860
+ setSrcChainOptions(sortChains(baseChains, sourceChainsDisabled));
861
+ setDstChainOptions(sortChains(baseChains, [...destinationChainsDisabled]));
862
+ }, [
863
+ supportedChains,
864
+ bridgeUI,
865
+ srcChain,
866
+ srcToken,
867
+ dstChain,
868
+ dstToken,
869
+ bridgeTokensDictionary,
870
+ ]);
706
871
 
707
- // Add new state to track initial setup
708
- const [isInitialStateSet, setIsInitialStateSet] = useState(false);
872
+ // =============================================================================
873
+ // useEffects responsible for state updates and validations
874
+ // =============================================================================
709
875
 
876
+ // sets the initial source chain based on the wallet's chain ID if not already initialized.
710
877
  useEffect(() => {
711
- // apply wallet chain only after initial state is set
712
878
  if (!walletChainId || !isInitialStateSet || integrationConfig?.srcChainId) {
713
879
  return;
714
880
  }
715
881
  const initSrcChain = supportedChains.find(
716
882
  ({ chainId }) => chainId === `${walletChainId}`,
717
883
  );
718
-
719
884
  if (initSrcChain) {
720
885
  setSrcChain(initSrcChain);
721
886
  }
@@ -726,10 +891,12 @@ export const SwapProvider = ({
726
891
  integrationConfig?.srcChainId,
727
892
  ]);
728
893
 
894
+ // resets source and destination tokens and chains when bridgeUI changes and initial state is not yet set.
729
895
  useEffect(() => {
730
- if (isInitialStateSet) {
896
+ if (!isInitialStateSet) {
731
897
  return;
732
898
  }
899
+
733
900
  setSrcToken(undefined);
734
901
  setDstToken(undefined);
735
902
  setSrcChain(undefined);
@@ -738,9 +905,8 @@ export const SwapProvider = ({
738
905
  // eslint-disable-next-line react-hooks/exhaustive-deps
739
906
  }, [bridgeUI]);
740
907
 
908
+ // This useEffect runs just once in full. It initializes source and destination chains and tokens based on integration config, wallet, and default values, then marks initial state as set.
741
909
  useEffect(() => {
742
- // Skip if initial state was already set or wait if supported chains are not loaded yet
743
-
744
910
  if (
745
911
  isInitialStateSet ||
746
912
  !supportedChains ||
@@ -750,70 +916,54 @@ export const SwapProvider = ({
750
916
  return;
751
917
  }
752
918
 
753
- let srcTokenAddress = integrationConfig.srcTokenAddr;
754
- let dstTokenAddress = integrationConfig.dstTokenAddr;
755
- // is it a valid eth address? if not try to find it by symbol / tokenId
756
- if (srcTokenAddress && !ethers.utils.isAddress(srcTokenAddress)) {
757
- srcTokenAddress = supportedChains
758
- .find((chain) => chain.chainId === integrationConfig?.srcChainId)
759
- ?.tokens.find(
760
- (token) => token.tokenId === integrationConfig?.srcTokenAddr,
761
- )
762
- ?.address?.toLowerCase();
763
- }
764
-
765
- if (dstTokenAddress && !ethers.utils.isAddress(dstTokenAddress)) {
766
- dstTokenAddress = supportedChains
767
- .find((chain) => chain.chainId === integrationConfig?.dstChainId)
768
- ?.tokens.find(
769
- (token) => token.tokenId === integrationConfig?.dstTokenAddr,
770
- )
771
- ?.address?.toLowerCase();
772
- }
773
-
774
- const searchedChainId =
775
- // currently set chain
919
+ // find chains
920
+ const searchedSrcChainId =
776
921
  srcChain ||
777
- // chain set by the integrator
778
922
  integrationConfig?.srcChainId ||
779
- // wallet chain
780
923
  (walletChainId ? `${walletChainId}` : undefined) ||
781
- // default chain
782
924
  (bridgeUI ? undefined : DEFAULT_SOURCE_CHAIN_ID);
783
925
 
784
926
  const initSrcChain = supportedChains.find(
785
- ({ chainId }) => chainId === searchedChainId,
927
+ ({ chainId }) => chainId === searchedSrcChainId,
786
928
  );
787
929
 
930
+ const searchedDstChainId =
931
+ dstChain ||
932
+ integrationConfig?.dstChainId ||
933
+ (bridgeUI ? undefined : DEFAULT_DESTINATION_CHAIN_ID);
934
+
935
+ const initDstChain = supportedChains.find(
936
+ ({ chainId }) => chainId === searchedDstChainId,
937
+ );
938
+
939
+ // find tokens
940
+ let initDstToken: Token | undefined;
788
941
  let initSrcToken: Token | undefined;
942
+
943
+ const srcTokenAddress = resolveTokenAddress(
944
+ integrationConfig.srcTokenAddr,
945
+ integrationConfig?.srcChainId,
946
+ supportedChains,
947
+ );
948
+ const dstTokenAddress = resolveTokenAddress(
949
+ integrationConfig.dstTokenAddr,
950
+ integrationConfig?.dstChainId,
951
+ supportedChains,
952
+ );
953
+
789
954
  if (initSrcChain) {
790
955
  initSrcToken = initSrcChain.tokens.find(
791
956
  ({ address }) =>
792
957
  address.toLowerCase() === srcTokenAddress?.toLowerCase(),
793
958
  );
794
959
  }
795
-
796
- let initDstChain: Chain | undefined = bridgeUI
797
- ? undefined
798
- : supportedChains.find(
799
- ({ chainId }) => chainId === DEFAULT_DESTINATION_CHAIN_ID,
800
- );
801
-
802
- // if integrator set it, use it
803
- if (integrationConfig.dstChainId) {
804
- initDstChain = supportedChains.find(
805
- ({ chainId }) => chainId === integrationConfig.dstChainId,
806
- );
807
- }
808
-
809
- let initDstToken: Token | undefined;
810
960
  if (initDstChain) {
811
961
  initDstToken = initDstChain.tokens.find(
812
962
  ({ address }) => address.toLowerCase() === dstTokenAddress,
813
963
  );
814
964
  }
815
965
 
816
- // the values here should not be from state but from the code above
966
+ // validate the initial state
817
967
  const { source, target } = mapToValidPath({
818
968
  source: { chain: initSrcChain, token: initSrcToken },
819
969
  target: { chain: initDstChain, token: initDstToken },
@@ -821,7 +971,6 @@ export const SwapProvider = ({
821
971
  supportedChains,
822
972
  bridgeTokensDictionary,
823
973
  });
824
-
825
974
  setSrcChain(source.chain);
826
975
  setDstChain(target.chain);
827
976
  setSrcToken(source.token);
@@ -840,7 +989,7 @@ export const SwapProvider = ({
840
989
  isInitialStateSet,
841
990
  bridgeTokensDictionary,
842
991
  ]);
843
-
992
+ // syncs current chain and token selections with the valid path computed from current state.
844
993
  useEffect(() => {
845
994
  if (
846
995
  !bridgeTokensDictionary ||
@@ -857,20 +1006,23 @@ export const SwapProvider = ({
857
1006
  supportedChains,
858
1007
  bridgeTokensDictionary,
859
1008
  });
860
-
861
1009
  if (source.chain?.chainId !== srcChain?.chainId) {
862
1010
  setSrcChain(source.chain);
863
1011
  }
864
-
865
1012
  if (target.chain?.chainId !== dstChain?.chainId) {
866
1013
  setDstChain(target.chain);
867
1014
  }
868
-
869
- if (source.token?.address !== srcToken?.address) {
1015
+ if (
1016
+ source.token?.address !== srcToken?.address ||
1017
+ source.token?.name !== srcToken?.name
1018
+ ) {
870
1019
  setSrcToken(source.token);
871
1020
  }
872
1021
 
873
- if (target.token?.address !== dstToken?.address) {
1022
+ if (
1023
+ target.token?.address !== dstToken?.address ||
1024
+ target.token?.name !== dstToken?.name
1025
+ ) {
874
1026
  setDstToken(target.token);
875
1027
  }
876
1028
  }, [
@@ -881,9 +1033,13 @@ export const SwapProvider = ({
881
1033
  bridgeUI,
882
1034
  supportedChains,
883
1035
  bridgeTokensDictionary,
1036
+ isInitialStateSet,
884
1037
  ]);
885
-
1038
+ // notifies integration or global callback when the destination chain changes.
886
1039
  useEffect(() => {
1040
+ if (!isInitialStateSet) {
1041
+ return;
1042
+ }
887
1043
  if (integrationConfig.onDstChainChange) {
888
1044
  integrationConfig.onDstChainChange(dstChain?.chainId);
889
1045
  } else if (window.xPayOnDstChainChange) {
@@ -891,8 +1047,11 @@ export const SwapProvider = ({
891
1047
  }
892
1048
  // eslint-disable-next-line react-hooks/exhaustive-deps
893
1049
  }, [integrationConfig.onDstChainChange, dstChain]);
894
-
1050
+ // notifies integration or global callback when the destination token changes.
895
1051
  useEffect(() => {
1052
+ if (!isInitialStateSet) {
1053
+ return;
1054
+ }
896
1055
  const token = dstToken
897
1056
  ? { address: dstToken.address.toLowerCase(), symbol: dstToken.tokenId }
898
1057
  : undefined;
@@ -904,8 +1063,11 @@ export const SwapProvider = ({
904
1063
  }
905
1064
  // eslint-disable-next-line react-hooks/exhaustive-deps
906
1065
  }, [integrationConfig.onDstTokenChange, dstToken]);
907
-
1066
+ // notifies integration or global callback when the source chain changes.
908
1067
  useEffect(() => {
1068
+ if (!isInitialStateSet) {
1069
+ return;
1070
+ }
909
1071
  if (integrationConfig.onSrcChainChange) {
910
1072
  integrationConfig.onSrcChainChange(srcChain?.chainId);
911
1073
  } else if (window.xPayOnSrcChainChange) {
@@ -913,8 +1075,11 @@ export const SwapProvider = ({
913
1075
  }
914
1076
  // eslint-disable-next-line react-hooks/exhaustive-deps
915
1077
  }, [integrationConfig.onSrcChainChange, srcChain]);
916
-
1078
+ // notifies integration or global callback when the source token changes.
917
1079
  useEffect(() => {
1080
+ if (!isInitialStateSet) {
1081
+ return;
1082
+ }
918
1083
  const token = srcToken
919
1084
  ? { address: srcToken.address.toLowerCase(), symbol: srcToken.tokenId }
920
1085
  : undefined;
@@ -927,18 +1092,6 @@ export const SwapProvider = ({
927
1092
  // eslint-disable-next-line react-hooks/exhaustive-deps
928
1093
  }, [integrationConfig.onSrcTokenChange, srcToken]);
929
1094
 
930
- const onConnectWallet = useCallback(() => {
931
- if (integrationConfig.onConnectWallet) {
932
- integrationConfig.onConnectWallet();
933
- } else if (window.xPayOnConnectWallet) {
934
- window.xPayOnConnectWallet();
935
- }
936
- }, [integrationConfig]);
937
-
938
- const hasOnConnectWalletCallback = useMemo(() => {
939
- return !!(integrationConfig.onConnectWallet || window.xPayOnConnectWallet);
940
- }, [integrationConfig.onConnectWallet]);
941
-
942
1095
  return (
943
1096
  <swapContext.Provider
944
1097
  value={{
@@ -956,6 +1109,8 @@ export const SwapProvider = ({
956
1109
  setSrcChain,
957
1110
  srcChainTokensOptions,
958
1111
  srcChainOtherTokensOptions,
1112
+ srcChainOptions,
1113
+ dstChainOptions,
959
1114
  tokenPrices,
960
1115
  dstChain,
961
1116
  setDstChain,
@@ -973,8 +1128,6 @@ export const SwapProvider = ({
973
1128
  dstValueWei,
974
1129
  dstValueUsd,
975
1130
  dstValueMin,
976
- lastTx,
977
- setLastTxHash,
978
1131
  expressDelivery,
979
1132
  setExpressDelivery,
980
1133
  infiniteApproval,
@@ -989,6 +1142,7 @@ export const SwapProvider = ({
989
1142
  srcTokenBalanceInfo,
990
1143
  isFetchingRoute,
991
1144
  isFetchingBalance,
1145
+ isFetchingBalances,
992
1146
  isExpressDeliveryActive,
993
1147
  quoteRoute,
994
1148
  refreshBalance,