@xswap-link/sdk 0.9.7 → 0.9.9

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.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "JavaScript SDK for XSwap platform",
5
5
  "homepage": "https://github.com/xswap-link/xswap-sdk",
6
6
  "repository": {
@@ -236,37 +236,43 @@ export const TokenPicker = ({
236
236
  // Create groups based on chain and wallet connection status
237
237
  const groups = chain
238
238
  ? [
239
- ...(!!address && ownedFilteredTokens.length
239
+ ...(!!address && ownedFilteredTokens.length && type === "source"
240
240
  ? [ownedFilteredTokens]
241
241
  : []),
242
242
  currentChainTokens,
243
243
  otherChainTokens,
244
244
  ]
245
245
  : [
246
- ...(!!address && ownedFilteredTokens.length
246
+ ...(!!address && ownedFilteredTokens.length && type === "source"
247
247
  ? [ownedFilteredTokens]
248
248
  : []),
249
249
  [...currentChainTokens, ...otherChainTokens],
250
250
  ];
251
251
 
252
252
  return groups.flat();
253
- }, [filteredTokens, otherFilteredTokens, chain, address, tokenPrices]);
253
+ }, [filteredTokens, otherFilteredTokens, chain, address, type, tokenPrices]);
254
254
 
255
255
  const ownedTokens = useMemo(
256
256
  () => allTokens.filter((token) => token.balance?.gt(0)),
257
257
  [allTokens],
258
258
  );
259
259
 
260
- const tokenGroups = chain
261
- ? [
262
- ...(!!address && ownedTokens.length ? [ownedTokens] : []),
263
- filteredTokens,
264
- otherFilteredTokens,
265
- ]
266
- : [
267
- ...(!!address && ownedTokens.length ? [ownedTokens] : []),
268
- otherFilteredTokens,
269
- ];
260
+ const tokenGroups = useMemo(() => {
261
+ return chain
262
+ ? [
263
+ ...(type === "source" && !!address && ownedTokens.length
264
+ ? [ownedTokens]
265
+ : []),
266
+ filteredTokens,
267
+ otherFilteredTokens,
268
+ ]
269
+ : [
270
+ ...(type === "source" && !!address && ownedTokens.length
271
+ ? [ownedTokens]
272
+ : []),
273
+ otherFilteredTokens,
274
+ ];
275
+ }, [chain, type, address, ownedTokens, filteredTokens, otherFilteredTokens]);
270
276
 
271
277
  return showImportWarning ? (
272
278
  <>
@@ -459,7 +465,7 @@ export const TokenPicker = ({
459
465
  if (!address) {
460
466
  return `${chain.displayName} network`;
461
467
  }
462
- return ownedTokens.length
468
+ return ownedTokens.length && type === "source"
463
469
  ? "My tokens"
464
470
  : `${chain.displayName} network`;
465
471
  }
@@ -483,7 +489,7 @@ export const TokenPicker = ({
483
489
  if (!address) {
484
490
  return "All networks";
485
491
  }
486
- return ownedTokens.length
492
+ return ownedTokens.length && type === "source"
487
493
  ? "My tokens"
488
494
  : "All networks";
489
495
  }
@@ -13,6 +13,7 @@ import { FC, MouseEvent, useEffect, useMemo } from "react";
13
13
  import { ErrorBoundary } from "react-error-boundary";
14
14
  import { useReconnect, WagmiProvider } from "wagmi";
15
15
  import { changeHostVariableColor } from "../../../localTheme";
16
+ import { setApiUrl } from "@src/services/api";
16
17
 
17
18
  export type TxConfigFormPayload = Omit<
18
19
  ModalIntegrationPayload,
@@ -97,6 +98,7 @@ export const TxConfigForm = ({
97
98
  onSrcChainChange,
98
99
  bridge,
99
100
  onConnectWallet,
101
+ override,
100
102
  }: TxConfigFormPayload &
101
103
  (
102
104
  | {
@@ -105,7 +107,6 @@ export const TxConfigForm = ({
105
107
  overlay: true;
106
108
  }
107
109
  | {
108
- // Satisfy TypeScript, so it understands that this field is not needed when overlay=false
109
110
  onSubmit?: undefined;
110
111
  onClose?: undefined;
111
112
  overlay: false;
@@ -129,6 +130,12 @@ export const TxConfigForm = ({
129
130
  changeHostVariableColor(lightTheme, styles);
130
131
  }, [lightTheme, styles]);
131
132
 
133
+ useEffect(() => {
134
+ if (override?.apiUrl) {
135
+ setApiUrl(override?.apiUrl);
136
+ }
137
+ }, [override?.apiUrl]);
138
+
132
139
  return (
133
140
  <ErrorBoundary
134
141
  FallbackComponent={() => (
@@ -214,6 +221,7 @@ export const openTxConfigForm = async ({
214
221
  dstChainLocked,
215
222
  srcTokenLocked,
216
223
  srcChainLocked,
224
+ override,
217
225
  }: TxConfigFormPayload): Promise<Route> => {
218
226
  try {
219
227
  if (xpayRoot === null) {
@@ -257,6 +265,7 @@ export const openTxConfigForm = async ({
257
265
  dstChainLocked={dstChainLocked}
258
266
  srcTokenLocked={srcTokenLocked}
259
267
  srcChainLocked={srcChainLocked}
268
+ override={override}
260
269
  />,
261
270
  );
262
271
  });
@@ -4,7 +4,7 @@ import {
4
4
  Web3Environment,
5
5
  WidgetIntegrationPayload,
6
6
  } from "@src/models";
7
- import { getChains } from "@src/services";
7
+ import { getChains, setApiUrl } from "@src/services";
8
8
  import { isServer } from "@src/utils";
9
9
  import { createWebComponent } from "@src/utils/webComponents";
10
10
  import { useEffect, useState } from "react";
@@ -35,6 +35,7 @@ const TxWidget = ({
35
35
  onSrcChainChange,
36
36
  onConnectWallet,
37
37
  bridge,
38
+ override,
38
39
  }: WidgetIntegrationPayload) => {
39
40
  const [loading, setLoading] = useState(true);
40
41
  const [supportedChains, setSupportedChains] = useState<Chain[]>([]);
@@ -43,6 +44,10 @@ const TxWidget = ({
43
44
  (async () => {
44
45
  setLoading(true);
45
46
 
47
+ if (override?.apiUrl) {
48
+ setApiUrl(override.apiUrl);
49
+ }
50
+
46
51
  const chains = (await getChains()).filter(
47
52
  ({ web3Environment, bridgeSupported, swapSupported }) =>
48
53
  web3Environment === Web3Environment.MAINNET &&
@@ -52,6 +57,7 @@ const TxWidget = ({
52
57
  setSupportedChains(chains);
53
58
  setLoading(false);
54
59
  })();
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
61
  }, []);
56
62
 
57
63
  const parsedStyles: ModalIntegrationStyles | undefined = (() => {
@@ -98,6 +104,7 @@ const TxWidget = ({
98
104
  onSrcChainChange={onSrcChainChange}
99
105
  onConnectWallet={onConnectWallet}
100
106
  bridge={bridge}
107
+ override={override}
101
108
  />
102
109
  )}
103
110
  </>
@@ -141,6 +148,7 @@ export const createTxWidgetWC = () => {
141
148
  onDstChainChange: "function",
142
149
  onSrcTokenChange: "function",
143
150
  onSrcChainChange: "function",
151
+ override: "json",
144
152
  },
145
153
  });
146
154
 
@@ -176,4 +184,5 @@ export type TxWidgetWCAttributes = {
176
184
  "on-src-token-change"?: WidgetIntegrationPayload["onSrcTokenChange"];
177
185
  "on-src-chain-change"?: WidgetIntegrationPayload["onSrcChainChange"];
178
186
  bridge: WidgetIntegrationPayload["bridge"] | undefined;
187
+ override: string | undefined;
179
188
  };
@@ -47,6 +47,7 @@ export const TxWidgetWCWrapped = ({
47
47
  onSrcChainChange,
48
48
  onConnectWallet,
49
49
  bridge,
50
+ override,
50
51
  }: WidgetIntegrationPayload) => {
51
52
  useEffect(() => {
52
53
  // We couldn't find a way to pass a function to the web component created by @r2wc/core.
@@ -84,6 +85,7 @@ export const TxWidgetWCWrapped = ({
84
85
  src-token-locked={srcTokenLocked}
85
86
  src-chain-locked={srcChainLocked}
86
87
  bridge={bridge}
88
+ override={JSON.stringify(override)}
87
89
  ></xpay-widget>
88
90
  );
89
91
  };
@@ -533,11 +533,32 @@ export const SwapProvider = ({
533
533
  !srcToken ||
534
534
  !dstToken ||
535
535
  !srcValueWei ||
536
- safeBigNumberFrom(srcValueWei).eq(0)
536
+ safeBigNumberFrom(srcValueWei).eq(0) ||
537
+ (bridgeUI && !bridgeTokensDictionary)
537
538
  ) {
538
539
  setRoute(undefined);
539
540
  return;
540
541
  }
542
+
543
+ // Validate the current path before requesting a route
544
+ const { source, target } = mapToValidPath({
545
+ source: { chain: srcChain, token: srcToken },
546
+ target: { chain: dstChain, token: dstToken },
547
+ type: bridgeUI ? "BRIDGE" : "SWAP",
548
+ supportedChains,
549
+ bridgeTokensDictionary: bridgeTokensDictionary || {},
550
+ });
551
+
552
+ // If any of the chains or tokens would be changed by validation, don't request a route
553
+ if (
554
+ source.chain?.chainId !== srcChain?.chainId ||
555
+ target.chain?.chainId !== dstChain?.chainId ||
556
+ source.token?.address !== srcToken?.address ||
557
+ target.token?.address !== dstToken?.address
558
+ ) {
559
+ return;
560
+ }
561
+
541
562
  try {
542
563
  setError("");
543
564
  setFetchingRoute(true);
@@ -580,6 +601,7 @@ export const SwapProvider = ({
580
601
  // eslint-disable-next-line react-hooks/exhaustive-deps
581
602
  }, [
582
603
  address,
604
+ bridgeUI,
583
605
  dstChain,
584
606
  dstToken,
585
607
  getRoute,
@@ -17,4 +17,3 @@ export * from "./TransactionHistory";
17
17
  export * from "./TxUIWrapper";
18
18
  export * from "./Web3Environment";
19
19
  export * from "./XSwapCallType";
20
- export * from "./XSwapConfig";
@@ -64,4 +64,7 @@ export type ModalIntegrationPayload = {
64
64
  onSrcChainChange?: (chain: string | undefined) => void;
65
65
  onConnectWallet?: () => void;
66
66
  bridge?: boolean;
67
+ override?: {
68
+ apiUrl?: string;
69
+ };
67
70
  };
@@ -27,4 +27,7 @@ export type WidgetIntegrationPayload = {
27
27
  onSrcChainChange?: (chain: string | undefined) => void;
28
28
  onConnectWallet?: () => void;
29
29
  bridge?: boolean;
30
+ override?: {
31
+ apiUrl?: string;
32
+ };
30
33
  };
@@ -13,13 +13,18 @@ import {
13
13
  TransactionHistory,
14
14
  TransactionStatus,
15
15
  } from "@src/models";
16
- import xSwapConfig from "../../xswap.config";
16
+
17
+ let XSWAP_API_URL = "https://xswap.link/api";
18
+
19
+ export const setApiUrl = (url: string) => {
20
+ XSWAP_API_URL = url;
21
+ };
17
22
 
18
23
  async function _sendRequest<T>(
19
24
  urlPath: string,
20
25
  options?: RequestInit,
21
26
  ): Promise<T> {
22
- const response = await fetch(`${xSwapConfig.apiUrl}${urlPath}`, {
27
+ const response = await fetch(`${XSWAP_API_URL}${urlPath}`, {
23
28
  method: "GET",
24
29
  headers: {
25
30
  "Content-Type": "application/json",
@@ -59,8 +64,7 @@ export async function getRoute(
59
64
  * @param ecosystem optional
60
65
  */
61
66
  export async function getChains(ecosystem?: Ecosystem): Promise<Chain[]> {
62
- return _sendRequest<Chain[]>(`/chains?data=${JSON.stringify({ ecosystem })}
63
- `);
67
+ return _sendRequest<Chain[]>(`/chains?data=${JSON.stringify({ ecosystem })}`);
64
68
  }
65
69
 
66
70
  /**
@@ -262,7 +262,14 @@ const mapToValidPathSwap = ({
262
262
  }
263
263
 
264
264
  // Setting tokens
265
- // Removed check for token chainId equality as tokens may not include a chainId property
265
+ // validate if the token is supported in the new source chain
266
+ newSourceToken = newSourceChain.tokens.find(
267
+ (token) => token.tokenId === newSourceToken?.tokenId && token.supported,
268
+ );
269
+ newTargetToken = newTargetChain.tokens.find(
270
+ (token) => token.tokenId === newTargetToken?.tokenId && token.supported,
271
+ );
272
+
266
273
  if (!newSourceToken) {
267
274
  newSourceToken = getDefaultSwapTokenForChain(
268
275
  newSourceChain,
@@ -339,18 +346,32 @@ const mapToValidPathBridge = ({
339
346
  // Setting tokens
340
347
  // check if the source token is bridgeable anywhere
341
348
  if (newSourceToken && newSourceChain) {
342
- const isBridgeable =
343
- Object.keys(
344
- bridgeTokensDictionary[newSourceChain.chainId]?.[
345
- newSourceToken.address.toLowerCase()
346
- ] || {},
347
- ).length > 0;
349
+ // First validate if the token is supported in the source chain
350
+ newSourceToken = newSourceChain.tokens.find(
351
+ (token) => token.tokenId === newSourceToken?.tokenId && token.supported,
352
+ );
348
353
 
349
- if (!isBridgeable) {
350
- newSourceToken = undefined;
354
+ if (newSourceToken) {
355
+ const isBridgeable =
356
+ Object.keys(
357
+ bridgeTokensDictionary[newSourceChain.chainId]?.[
358
+ newSourceToken.address.toLowerCase()
359
+ ] || {},
360
+ ).length > 0;
361
+
362
+ if (!isBridgeable) {
363
+ newSourceToken = undefined;
364
+ }
351
365
  }
352
366
  }
353
367
 
368
+ // Validate if target token is supported in the target chain
369
+ if (newTargetToken && newTargetChain) {
370
+ newTargetToken = newTargetChain.tokens.find(
371
+ (token) => token.tokenId === newTargetToken?.tokenId && token.supported,
372
+ );
373
+ }
374
+
354
375
  if (!newSourceToken) {
355
376
  const defaultToken = getDefaultBridgeTokenForChain({
356
377
  chain: newSourceChain,
@@ -260,6 +260,110 @@ describe("SwapProvider", () => {
260
260
  // Should set a valid token for the new chain
261
261
  expect(result.current.srcToken?.address).toBeDefined();
262
262
  });
263
+
264
+ it("should preserve token when changing to chain with same token", async () => {
265
+ const { result } = renderHook(() => useSwapContext(), {
266
+ wrapper: getWrapper(
267
+ { ...integrationConfig, bridge: false },
268
+ supportedChains,
269
+ wagmiConfig,
270
+ ),
271
+ });
272
+
273
+ await act(async () => {
274
+ await new Promise((resolve) => setTimeout(resolve, 0));
275
+ });
276
+
277
+ // First set initial chain and token
278
+ const initialChain = supportedChains.find(
279
+ (chain) => chain.chainId === SRC_SWAP_CHAIN_ID,
280
+ );
281
+ if (!initialChain) throw new Error("Initial chain not found");
282
+
283
+ const initialToken = initialChain.tokens.find(
284
+ (token) =>
285
+ token.address.toLowerCase() === SRC_SWAP_TOKEN_ADDR.toLowerCase(),
286
+ );
287
+ if (!initialToken) throw new Error("Initial token not found");
288
+
289
+ await act(async () => {
290
+ result.current.setSrcChain(initialChain);
291
+ result.current.setSrcToken(initialToken);
292
+ await new Promise((resolve) => setTimeout(resolve, 0));
293
+ });
294
+
295
+ // Now change to a different chain that has the same token (by tokenId)
296
+ const newChain = supportedChains.find(
297
+ (chain) =>
298
+ chain.chainId !== initialChain.chainId &&
299
+ chain.tokens.some(
300
+ (token) =>
301
+ token.tokenId === initialToken.tokenId && token.supported,
302
+ ),
303
+ );
304
+ if (!newChain) throw new Error("New chain with same token not found");
305
+
306
+ await act(async () => {
307
+ result.current.setSrcChain(newChain);
308
+ await new Promise((resolve) => setTimeout(resolve, 0));
309
+ });
310
+
311
+ // Should preserve the same tokenId but with new chain's token address
312
+ expect(result.current.srcToken?.tokenId).toBe(initialToken.tokenId);
313
+ expect(result.current.srcToken?.address).not.toBe(initialToken.address);
314
+ });
315
+
316
+ it("should reset token when changing to chain without same token", async () => {
317
+ const { result } = renderHook(() => useSwapContext(), {
318
+ wrapper: getWrapper(
319
+ { ...integrationConfig, bridge: false },
320
+ supportedChains,
321
+ wagmiConfig,
322
+ ),
323
+ });
324
+
325
+ await act(async () => {
326
+ await new Promise((resolve) => setTimeout(resolve, 0));
327
+ });
328
+
329
+ // First set initial chain and token
330
+ const initialChain = supportedChains.find(
331
+ (chain) => chain.chainId === SRC_SWAP_CHAIN_ID,
332
+ );
333
+ if (!initialChain) throw new Error("Initial chain not found");
334
+
335
+ const initialToken = initialChain.tokens.find(
336
+ (token) =>
337
+ token.address.toLowerCase() === SRC_SWAP_TOKEN_ADDR.toLowerCase(),
338
+ );
339
+ if (!initialToken) throw new Error("Initial token not found");
340
+
341
+ await act(async () => {
342
+ result.current.setSrcChain(initialChain);
343
+ result.current.setSrcToken(initialToken);
344
+ await new Promise((resolve) => setTimeout(resolve, 0));
345
+ });
346
+
347
+ // Now change to a chain that doesn't have the same token
348
+ const newChain = supportedChains.find(
349
+ (chain) =>
350
+ chain.chainId !== initialChain.chainId &&
351
+ !chain.tokens.some((token) => token.tokenId === initialToken.tokenId),
352
+ );
353
+ if (!newChain) throw new Error("New chain without same token not found");
354
+
355
+ await act(async () => {
356
+ result.current.setSrcChain(newChain);
357
+ await new Promise((resolve) => setTimeout(resolve, 0));
358
+ });
359
+
360
+ // Should have a different token now
361
+ expect(result.current.srcToken?.tokenId).not.toBe(initialToken.tokenId);
362
+ expect(result.current.srcToken?.address).not.toBe(initialToken.address);
363
+ // But should still have a valid token
364
+ expect(result.current.srcToken).toBeDefined();
365
+ });
366
+
263
367
  it("should handle destination chain change", async () => {
264
368
  const { result } = renderHook(() => useSwapContext(), {
265
369
  wrapper: getWrapper(
@@ -288,7 +392,7 @@ describe("SwapProvider", () => {
288
392
  expect(result.current.dstToken?.address).toBeDefined();
289
393
  });
290
394
 
291
- it("should handle source token change", async () => {
395
+ it("should preserve token when changing destination chain with same token", async () => {
292
396
  const { result } = renderHook(() => useSwapContext(), {
293
397
  wrapper: getWrapper(
294
398
  { ...integrationConfig, bridge: false },
@@ -301,12 +405,119 @@ describe("SwapProvider", () => {
301
405
  await new Promise((resolve) => setTimeout(resolve, 0));
302
406
  });
303
407
 
408
+ // First set initial chain and token
409
+ const initialChain = supportedChains.find(
410
+ (chain) => chain.chainId === DST_SWAP_CHAIN_ID,
411
+ );
412
+ if (!initialChain) throw new Error("Initial chain not found");
413
+
414
+ const initialToken = initialChain.tokens.find(
415
+ (token) =>
416
+ token.address.toLowerCase() === DST_SWAP_TOKEN_ADDR.toLowerCase(),
417
+ );
418
+ if (!initialToken) throw new Error("Initial token not found");
419
+
420
+ await act(async () => {
421
+ result.current.setDstChain(initialChain);
422
+ result.current.setDstToken(initialToken);
423
+ await new Promise((resolve) => setTimeout(resolve, 0));
424
+ });
425
+
426
+ // Now change to a different chain that has the same token (by tokenId)
304
427
  const newChain = supportedChains.find(
428
+ (chain) =>
429
+ chain.chainId !== initialChain.chainId &&
430
+ chain.tokens.some(
431
+ (token) =>
432
+ token.tokenId === initialToken.tokenId && token.supported,
433
+ ),
434
+ );
435
+ if (!newChain) throw new Error("New chain with same token not found");
436
+
437
+ await act(async () => {
438
+ result.current.setDstChain(newChain);
439
+ await new Promise((resolve) => setTimeout(resolve, 0));
440
+ });
441
+
442
+ // Should preserve the same tokenId but with new chain's token address
443
+ expect(result.current.dstToken?.tokenId).toBe(initialToken.tokenId);
444
+ expect(result.current.dstToken?.address).not.toBe(initialToken.address);
445
+ });
446
+
447
+ it("should reset token when changing destination chain without same token", async () => {
448
+ const { result } = renderHook(() => useSwapContext(), {
449
+ wrapper: getWrapper(
450
+ { ...integrationConfig, bridge: false },
451
+ supportedChains,
452
+ wagmiConfig,
453
+ ),
454
+ });
455
+
456
+ await act(async () => {
457
+ await new Promise((resolve) => setTimeout(resolve, 0));
458
+ });
459
+
460
+ // First set initial chain and token
461
+ const initialChain = supportedChains.find(
462
+ (chain) => chain.chainId === DST_SWAP_CHAIN_ID,
463
+ );
464
+ if (!initialChain) throw new Error("Initial chain not found");
465
+
466
+ const initialToken = initialChain.tokens.find(
467
+ (token) =>
468
+ token.address.toLowerCase() === DST_SWAP_TOKEN_ADDR.toLowerCase(),
469
+ );
470
+ if (!initialToken) throw new Error("Initial token not found");
471
+
472
+ await act(async () => {
473
+ result.current.setDstChain(initialChain);
474
+ result.current.setDstToken(initialToken);
475
+ await new Promise((resolve) => setTimeout(resolve, 0));
476
+ });
477
+
478
+ // Now change to a chain that doesn't have the same token
479
+ const newChain = supportedChains.find(
480
+ (chain) =>
481
+ chain.chainId !== initialChain.chainId &&
482
+ !chain.tokens.some((token) => token.tokenId === initialToken.tokenId),
483
+ );
484
+ if (!newChain) throw new Error("New chain without same token not found");
485
+
486
+ await act(async () => {
487
+ result.current.setDstChain(newChain);
488
+ await new Promise((resolve) => setTimeout(resolve, 0));
489
+ });
490
+
491
+ // Should have a different token now
492
+ expect(result.current.dstToken?.tokenId).not.toBe(initialToken.tokenId);
493
+ expect(result.current.dstToken?.address).not.toBe(initialToken.address);
494
+ // But should still have a valid token
495
+ expect(result.current.dstToken).toBeDefined();
496
+ });
497
+
498
+ it("should handle source token change", async () => {
499
+ const { result } = renderHook(() => useSwapContext(), {
500
+ wrapper: getWrapper(
501
+ {
502
+ ...integrationConfig,
503
+ bridge: false,
504
+ srcChainId: SRC_SWAP_CHAIN_ID,
505
+ },
506
+ supportedChains,
507
+ wagmiConfig,
508
+ ),
509
+ });
510
+
511
+ await act(async () => {
512
+ await new Promise((resolve) => setTimeout(resolve, 0));
513
+ });
514
+
515
+ const activeSrcChain = supportedChains.find(
305
516
  (chain) => chain.chainId === SRC_SWAP_CHAIN_ID,
306
517
  );
307
- if (!newChain) throw new Error("Source chain not set");
518
+ if (!activeSrcChain) throw new Error("Source chain not set");
308
519
 
309
- const newToken = newChain.tokens.find(
520
+ const newToken = activeSrcChain.tokens.find(
310
521
  (token) =>
311
522
  token.address.toLowerCase() === SRC_SWAP_TOKEN_ADDR.toLowerCase(),
312
523
  );
@@ -322,7 +533,11 @@ describe("SwapProvider", () => {
322
533
  it("should handle destination token change", async () => {
323
534
  const { result } = renderHook(() => useSwapContext(), {
324
535
  wrapper: getWrapper(
325
- { ...integrationConfig, bridge: false },
536
+ {
537
+ ...integrationConfig,
538
+ bridge: false,
539
+ dstChainId: DST_SWAP_CHAIN_ID,
540
+ },
326
541
  supportedChains,
327
542
  wagmiConfig,
328
543
  ),
@@ -544,6 +759,61 @@ describe("SwapProvider", () => {
544
759
  result.current.dstChain?.chainId,
545
760
  );
546
761
  });
762
+
763
+ it("should preserve token when changing destination chain if token exists on both chains", async () => {
764
+ // Set up initial state with Arbitrum as source chain
765
+ const arbitrumChain = supportedChains.find(
766
+ (chain) => chain.chainId === "42161", // Arbitrum One
767
+ );
768
+ if (!arbitrumChain) throw new Error("Arbitrum chain not found");
769
+
770
+ // Find WETH token on Base
771
+ const arbitrumWethToken = arbitrumChain.tokens.find(
772
+ (token) => token.tokenId === "WETH",
773
+ );
774
+ if (!arbitrumWethToken)
775
+ throw new Error("WETH token not found on arbitrum");
776
+
777
+ // Set Base as initial destination chain
778
+ const baseChain = supportedChains.find(
779
+ (chain) => chain.chainId === "8453", // Base
780
+ );
781
+ if (!baseChain) throw new Error("Base chain not found");
782
+
783
+ const { result } = renderHook(() => useSwapContext(), {
784
+ wrapper: getWrapper(
785
+ {
786
+ ...integrationConfig,
787
+ bridge: true,
788
+ srcChainId: arbitrumChain.chainId,
789
+ dstChainId: baseChain.chainId,
790
+ srcTokenAddr: arbitrumWethToken.address,
791
+ },
792
+ supportedChains,
793
+ wagmiConfig,
794
+ ),
795
+ });
796
+
797
+ await act(async () => {
798
+ await new Promise((resolve) => setTimeout(resolve, 0));
799
+ });
800
+
801
+ // Find Ethereum chain
802
+ const ethereumChain = supportedChains.find(
803
+ (chain) => chain.chainId === "1", // Ethereum
804
+ );
805
+ if (!ethereumChain) throw new Error("Ethereum chain not found");
806
+
807
+ // Change destination chain to Ethereum
808
+ await act(async () => {
809
+ result.current.setDstChain(ethereumChain);
810
+ await new Promise((resolve) => setTimeout(resolve, 0));
811
+ });
812
+
813
+ // Verify that WETH is still selected but with Ethereum's address
814
+ expect(result.current.dstToken?.tokenId).toBe("WETH");
815
+ expect(result.current.dstChain?.chainId).toBe("1");
816
+ });
547
817
  });
548
818
 
549
819
  describe("Swap mode validation", () => {
@@ -1,4 +0,0 @@
1
- export type XSwapConfig = {
2
- apiUrl: string;
3
- localCss: boolean;
4
- };