@xswap-link/sdk 0.9.6 → 0.9.8

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +0 -18
  3. package/dist/index.d.mts +13 -8
  4. package/dist/index.d.ts +13 -8
  5. package/dist/index.global.js +63 -64
  6. package/dist/index.js +9 -10
  7. package/dist/index.mjs +9 -10
  8. package/localTheme.ts +3 -0
  9. package/package.json +1 -1
  10. package/src/assets/icons/ChainlinkIcon.tsx +21 -0
  11. package/src/assets/icons/index.ts +1 -1
  12. package/src/components/PoweredBy/index.tsx +3 -18
  13. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/index.tsx +13 -97
  14. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/index.tsx +3 -82
  15. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +8 -11
  16. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +0 -1
  17. package/src/components/TxConfigForm/index.tsx +10 -1
  18. package/src/components/TxWidgetWC/index.tsx +10 -1
  19. package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
  20. package/src/context/SwapProvider.tsx +454 -300
  21. package/src/models/TokenData.ts +2 -0
  22. package/src/models/index.ts +0 -1
  23. package/src/models/payloads/ModalIntegrationPayload.ts +4 -0
  24. package/src/models/payloads/WidgetIntegrationPayload.ts +3 -0
  25. package/src/services/api.ts +8 -4
  26. package/src/utils/index.ts +30 -0
  27. package/src/utils/validation.ts +19 -25
  28. package/tailwind.config.js +2 -0
  29. package/test/context/SwapProvider.test.tsx +220 -4
  30. package/test/fixtures/supportedChains.mock.ts +7180 -22795
  31. package/src/assets/icons/ChainlinkCCIPIcon.tsx +0 -23
  32. package/src/models/XSwapConfig.ts +0 -4
  33. package/xswap.config.ts +0 -20
@@ -19,6 +19,8 @@ export type Chain = {
19
19
  scanApiURL?: string;
20
20
  defaultDstChain?: string;
21
21
  disabledForDestination?: string[];
22
+ supportedDstForBridge?: string[];
23
+ supportedDstForSwap?: string[];
22
24
  };
23
25
 
24
26
  export type Token = {
@@ -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";
@@ -29,6 +29,7 @@ export type ModalIntegrationThemeStyles = {
29
29
  warningLight?: string;
30
30
  errorDark?: string;
31
31
  errorLight?: string;
32
+ chainlinkLogo?: string;
32
33
  };
33
34
 
34
35
  export type ModalIntegrationStyles = Pick<
@@ -63,4 +64,7 @@ export type ModalIntegrationPayload = {
63
64
  onSrcChainChange?: (chain: string | undefined) => void;
64
65
  onConnectWallet?: () => void;
65
66
  bridge?: boolean;
67
+ override?: {
68
+ apiUrl?: string;
69
+ };
66
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
  /**
@@ -3,7 +3,9 @@ export * from "./numbers";
3
3
  export * from "./parseWeb3Error";
4
4
  export * from "./strings";
5
5
 
6
+ import { Chain } from "@src/models";
6
7
  import { format } from "date-fns";
8
+ import { ethers } from "ethers";
7
9
 
8
10
  // eslint-disable-next-line
9
11
  export const replaceNull = (values: any[], newValue: any) => {
@@ -58,3 +60,31 @@ export const getDate = (blockTimestamp: number) => {
58
60
  };
59
61
 
60
62
  export const isServer = typeof window === "undefined";
63
+
64
+ // Helper function to validate and resolve token addresses
65
+ export const resolveTokenAddress = (
66
+ tokenAddressOrId: string | undefined,
67
+ chainId: string | undefined,
68
+ chains: Chain[],
69
+ ): string | undefined => {
70
+ if (!tokenAddressOrId || ethers.utils.isAddress(tokenAddressOrId)) {
71
+ return tokenAddressOrId;
72
+ }
73
+ // try to find the token by the tokenId
74
+ return chains
75
+ .find((chain) => chain.chainId === chainId)
76
+ ?.tokens.find((token) => token.tokenId === tokenAddressOrId)
77
+ ?.address?.toLowerCase();
78
+ };
79
+
80
+ export const sortChains = (chains: Chain[], disabledChainIds: string[]) =>
81
+ [...chains]
82
+
83
+ .map((chain) => ({
84
+ ...chain,
85
+ disabled: disabledChainIds.includes(chain.chainId),
86
+ }))
87
+ .sort((a, b) => {
88
+ if (a.disabled === b.disabled) return 0;
89
+ return a.disabled ? 1 : -1;
90
+ });
@@ -121,7 +121,7 @@ const isSwapTargetChainValid = ({
121
121
 
122
122
  return (
123
123
  targetChain.swapSupported &&
124
- !sourceChain.disabledForDestination?.includes(targetChain.chainId)
124
+ sourceChain.supportedDstForSwap?.includes(targetChain.chainId)
125
125
  );
126
126
  };
127
127
 
@@ -138,9 +138,16 @@ const isBridgeTargetValid = ({
138
138
  sourceToken: Token | undefined;
139
139
  }) => {
140
140
  // We set it to a valid state initially.
141
+ let isSupportedDstForBridge = true;
141
142
  let isSameToken = true;
142
143
  let isSameChain = false;
143
144
 
145
+ if (sourceChain && targetChain) {
146
+ isSupportedDstForBridge = !!sourceChain.supportedDstForBridge?.includes(
147
+ targetChain.chainId,
148
+ );
149
+ }
150
+
144
151
  if (sourceToken && targetChain) {
145
152
  // Bridge target chain is invalid when it doesn't have the same token available as selected in the source.
146
153
  isSameToken = !!targetChain.tokens.find(
@@ -154,7 +161,7 @@ const isBridgeTargetValid = ({
154
161
  isSameChain = sourceChain.chainId === targetChain.chainId;
155
162
  }
156
163
 
157
- return isSameToken && !isSameChain;
164
+ return isSameToken && !isSameChain && isSupportedDstForBridge;
158
165
  };
159
166
 
160
167
  type MapToValidPathParams = {
@@ -255,6 +262,13 @@ const mapToValidPathSwap = ({
255
262
  }
256
263
 
257
264
  // Setting tokens
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
+ );
258
272
 
259
273
  if (!newSourceToken) {
260
274
  newSourceToken = getDefaultSwapTokenForChain(
@@ -262,6 +276,7 @@ const mapToValidPathSwap = ({
262
276
  DEFAULT_SOURCE_TOKEN_ADDR,
263
277
  );
264
278
  }
279
+
265
280
  if (!newTargetToken || newSourceToken === newTargetToken) {
266
281
  newTargetToken = getDefaultSwapTokenForChain(
267
282
  newTargetChain,
@@ -315,10 +330,8 @@ const mapToValidPathBridge = ({
315
330
  sourceToken: newSourceToken,
316
331
  })
317
332
  ) {
318
- newTargetChain = undefined;
319
333
  // We can't have dangling token from a different chain.
320
334
  newTargetToken = undefined;
321
-
322
335
  if (
323
336
  !isBridgeTargetValid({
324
337
  sourceChain: newSourceChain,
@@ -326,22 +339,7 @@ const mapToValidPathBridge = ({
326
339
  sourceToken: newSourceToken,
327
340
  })
328
341
  ) {
329
- // This should never occur if our app is configured properly.
330
- console.error("[WRONG CONFIG] default bridge target chain is invalid.");
331
-
332
- // Find any chain that is valid.
333
- newTargetChain = supportedChains.find((chain) =>
334
- isBridgeTargetValid({
335
- sourceChain: newSourceChain,
336
- targetChain: chain,
337
- sourceToken: newSourceToken,
338
- }),
339
- );
340
- if (!newTargetChain) {
341
- throw new Error(
342
- "mapToValidSwap > None of the chains supports bridge. Couldn't set target chain.",
343
- );
344
- }
342
+ newTargetChain = undefined;
345
343
  }
346
344
  }
347
345
 
@@ -365,7 +363,6 @@ const mapToValidPathBridge = ({
365
363
  chain: newSourceChain,
366
364
  bridgeTokensDictionary,
367
365
  });
368
-
369
366
  if (defaultToken && newSourceChain && newTargetChain) {
370
367
  const isDefaultTokenBridgeable =
371
368
  !!bridgeTokensDictionary[newSourceChain.chainId]?.[
@@ -375,9 +372,6 @@ const mapToValidPathBridge = ({
375
372
  if (isDefaultTokenBridgeable) {
376
373
  newSourceToken = defaultToken;
377
374
  } else {
378
- // This should never occur if our app is configured properly.
379
- console.error("[WRONG CONFIG] default bridge source token is invalid.");
380
- // Default is invalid, so let's keep it unset.
381
375
  newSourceToken = undefined;
382
376
  }
383
377
  } else {
@@ -409,7 +403,7 @@ const mapToValidPathBridge = ({
409
403
  }
410
404
  }
411
405
 
412
- if (!newTargetToken) {
406
+ if (!newTargetToken && newSourceToken) {
413
407
  newTargetChain = undefined;
414
408
  }
415
409
 
@@ -64,6 +64,8 @@ export default {
64
64
  "color-mix(in srgb, var(--error-dark) calc(<alpha-value> * 100%), transparent)",
65
65
  t_error_light:
66
66
  "color-mix(in srgb, var(--error-light) calc(<alpha-value> * 100%), transparent)",
67
+ t_chainlink_logo:
68
+ "color-mix(in srgb, var(--chainlink-logo) calc(<alpha-value> * 100%), transparent)",
67
69
 
68
70
  x_alert: "rgb(255,183,77)",
69
71
  x_alert_light: "rgb(255,226,183)",
@@ -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)
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
304
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
  ),
@@ -808,6 +1023,7 @@ describe("SwapProvider", () => {
808
1023
  {
809
1024
  ...integrationConfig,
810
1025
  bridge: true,
1026
+ srcChainId: SRC_BRIDGE_CHAIN_ID,
811
1027
  dstChainId: INVALID_DST_BRIDGE_CHAIN_ID,
812
1028
  },
813
1029
  supportedChains,