@xswap-link/sdk 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.github/workflows/main.yml +2 -1
  2. package/CHANGELOG.md +6 -0
  3. package/babel.config.cjs +8 -0
  4. package/dist/index.global.js +128 -131
  5. package/dist/index.js +9 -9
  6. package/dist/index.mjs +10 -10
  7. package/jest.config.ts +37 -0
  8. package/package.json +17 -5
  9. package/src/components/Modal/index.tsx +1 -1
  10. package/src/components/Swap/Header/index.tsx +1 -1
  11. package/src/components/Swap/ReorderButton/ReorderButton.tsx +12 -10
  12. package/src/components/Swap/SwapView/FeesPanel/index.tsx +1 -1
  13. package/src/components/Swap/SwapView/SwapButton/index.tsx +23 -21
  14. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +4 -2
  15. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +11 -2
  16. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +116 -33
  17. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +1 -1
  18. package/src/components/Swap/SwapView/index.tsx +1 -1
  19. package/src/context/HistoryProvider.tsx +2 -2
  20. package/src/context/SwapProvider.tsx +147 -62
  21. package/src/context/TransactionProvider.tsx +1 -1
  22. package/src/utils/validation.ts +50 -16
  23. package/tailwind.config.js +1 -0
  24. package/test/context/SwapProvider.test.tsx +851 -0
  25. package/test/fileMock.ts +1 -0
  26. package/test/fixtures/bridgeTokens.mock.ts +1318 -0
  27. package/test/fixtures/bridgeTokensDictionary.mock.ts +1272 -0
  28. package/test/fixtures/integrationConfig.mock.ts +10 -0
  29. package/test/fixtures/supportedChains.mock.ts +32950 -0
  30. package/test/{api.test.ts → services/getChains.test.ts} +6 -5
  31. package/test/setup.ts +13 -0
  32. package/test/styleMock.ts +1 -0
  33. package/jest.config.json +0 -8
  34. package/test/api.mock.ts +0 -106
  35. package/test/setupTests.ts +0 -3
@@ -1,10 +1,8 @@
1
1
  import { TxConfigFormPayload } from "@src/components";
2
2
  import {
3
3
  DEFAULT_DESTINATION_CHAIN_ID,
4
- DEFAULT_DESTINATION_TOKEN_ADDR,
5
4
  DEFAULT_REQUEST_ABORT_MSG,
6
5
  DEFAULT_SOURCE_CHAIN_ID,
7
- DEFAULT_SOURCE_TOKEN_ADDR,
8
6
  } from "@src/constants";
9
7
  import { ADDRESSES } from "@src/contracts";
10
8
  import {
@@ -32,9 +30,8 @@ import {
32
30
  safeBigNumberFrom,
33
31
  weiToHumanReadable,
34
32
  } from "@src/utils";
35
- import { mapToValidPath } from "@src/utils/validation";
36
33
  import BigNumberJS from "bignumber.js";
37
- import { BigNumber, constants } from "ethers";
34
+ import { BigNumber, constants, ethers } from "ethers";
38
35
  import {
39
36
  createContext,
40
37
  Dispatch,
@@ -49,6 +46,7 @@ import {
49
46
  } from "react";
50
47
  import { Config, useAccount } from "wagmi";
51
48
  import { useHistory } from "./HistoryProvider";
49
+ import { mapToValidPath } from "@src/utils/validation";
52
50
 
53
51
  type Tab = "Swap" | "History" | "Settings";
54
52
  type SwapContext = {
@@ -150,7 +148,7 @@ const init: SwapContext = {
150
148
  dstValueMin: "",
151
149
  lastTx: undefined,
152
150
  expressDelivery: true,
153
- infiniteApproval: false,
151
+ infiniteApproval: true,
154
152
  slippage: "1.5",
155
153
  route: undefined,
156
154
  isAsyncDataLoaded: false,
@@ -344,8 +342,8 @@ export const SwapProvider = ({
344
342
  }, [bridgeTokens]);
345
343
 
346
344
  const getTokenOptions = useCallback(
347
- (chainId: string | undefined, oppositeChainId: string | undefined) => {
348
- if (!chainId || !oppositeChainId) {
345
+ (chainId: string | undefined) => {
346
+ if (!chainId) {
349
347
  return [];
350
348
  }
351
349
 
@@ -564,7 +562,6 @@ export const SwapProvider = ({
564
562
  // source chain
565
563
  const srcTokenOptions: TokenOption[] = getTokenOptions(
566
564
  srcChain?.chainId,
567
- dstChain?.chainId,
568
565
  ).filter(({ symbol }) =>
569
566
  dstChain?.chainId === srcChain?.chainId
570
567
  ? symbol !== dstToken?.symbol
@@ -575,7 +572,6 @@ export const SwapProvider = ({
575
572
  // destination chain
576
573
  const dstTokenOptions: TokenOption[] = getTokenOptions(
577
574
  dstChain?.chainId,
578
- dstChain?.chainId,
579
575
  ).filter(({ symbol }) =>
580
576
  dstChain?.chainId === srcChain?.chainId
581
577
  ? symbol !== srcToken?.symbol
@@ -603,7 +599,7 @@ export const SwapProvider = ({
603
599
  );
604
600
  for (const { chainId } of srcOtherChains) {
605
601
  srcOtherTokenOptions.push(
606
- ...getTokenOptions(chainId, dstChain?.chainId).filter(({ symbol }) =>
602
+ ...getTokenOptions(chainId).filter(({ symbol }) =>
607
603
  chainId === dstChain?.chainId ? symbol !== dstToken?.symbol : true,
608
604
  ),
609
605
  );
@@ -620,7 +616,7 @@ export const SwapProvider = ({
620
616
  );
621
617
  for (const { chainId } of dstOtherChains) {
622
618
  dstOtherTokenOptions.push(
623
- ...getTokenOptions(chainId, dstChain?.chainId).filter(({ symbol }) =>
619
+ ...getTokenOptions(chainId).filter(({ symbol }) =>
624
620
  chainId === srcChain?.chainId ? symbol !== srcToken?.symbol : true,
625
621
  ),
626
622
  );
@@ -714,64 +710,149 @@ export const SwapProvider = ({
714
710
  );
715
711
  }, [srcChain]);
716
712
 
713
+ // Add new state to track initial setup
714
+ const [isInitialStateSet, setIsInitialStateSet] = useState(false);
715
+
717
716
  useEffect(() => {
718
- setSrcChain((state) => {
719
- const searchedChainId =
720
- // currently set chain
721
- state?.chainId ||
722
- // chain set by the integrator
723
- integrationConfig?.srcChainId ||
724
- // wallet chain
725
- (walletChainId ? `${walletChainId}` : undefined) ||
726
- // default chain
727
- (bridgeUI ? undefined : DEFAULT_SOURCE_CHAIN_ID);
728
-
729
- const initSrcChain = supportedChains.find(
730
- ({ chainId }) => chainId === searchedChainId,
731
- );
717
+ // apply wallet chain only after initial state is set
718
+ if (!walletChainId || !isInitialStateSet || integrationConfig?.srcChainId) {
719
+ return;
720
+ }
721
+ const initSrcChain = supportedChains.find(
722
+ ({ chainId }) => chainId === `${walletChainId}`,
723
+ );
732
724
 
733
- if (initSrcChain) {
734
- const initSrcToken = initSrcChain.tokens.find(
735
- ({ address }) =>
736
- address.toLowerCase() ===
737
- (integrationConfig?.srcTokenAddr?.toLowerCase() ||
738
- DEFAULT_SOURCE_TOKEN_ADDR.toLowerCase()),
739
- );
725
+ if (initSrcChain) {
726
+ setSrcChain(initSrcChain);
727
+ }
728
+ }, [
729
+ walletChainId,
730
+ isInitialStateSet,
731
+ supportedChains,
732
+ integrationConfig?.srcChainId,
733
+ ]);
740
734
 
741
- // This useEffect is only triggered by config and wallet changes.
742
- // So it will run when user connects wallet.
743
- // We want to keep the token that was already selected before connecting and only use initSrcToken if the token filed was empty.
744
- setSrcToken((state) => state || initSrcToken);
745
- } else {
746
- setSrcToken(undefined);
747
- }
748
- return initSrcChain;
749
- });
735
+ useEffect(() => {
736
+ if (isInitialStateSet) {
737
+ return;
738
+ }
739
+ setSrcToken(undefined);
740
+ setDstToken(undefined);
741
+ setSrcChain(undefined);
742
+ setDstChain(undefined);
743
+
744
+ // eslint-disable-next-line react-hooks/exhaustive-deps
745
+ }, [bridgeUI]);
746
+
747
+ useEffect(() => {
748
+ // Skip if initial state was already set or wait if supported chains are not loaded yet
750
749
 
751
- const initDstChain = bridgeUI
750
+ if (
751
+ isInitialStateSet ||
752
+ !supportedChains ||
753
+ supportedChains.length === 0 ||
754
+ !bridgeTokensDictionary
755
+ ) {
756
+ return;
757
+ }
758
+
759
+ let srcTokenAddress = integrationConfig.srcTokenAddr;
760
+ let dstTokenAddress = integrationConfig.dstTokenAddr;
761
+ // is it a valid eth address? if not try to find it by symbol / tokenId
762
+ if (srcTokenAddress && !ethers.utils.isAddress(srcTokenAddress)) {
763
+ srcTokenAddress = supportedChains
764
+ .find((chain) => chain.chainId === integrationConfig?.srcChainId)
765
+ ?.tokens.find(
766
+ (token) => token.tokenId === integrationConfig?.srcTokenAddr,
767
+ )
768
+ ?.address?.toLowerCase();
769
+ }
770
+
771
+ if (dstTokenAddress && !ethers.utils.isAddress(dstTokenAddress)) {
772
+ dstTokenAddress = supportedChains
773
+ .find((chain) => chain.chainId === integrationConfig?.dstChainId)
774
+ ?.tokens.find(
775
+ (token) => token.tokenId === integrationConfig?.dstTokenAddr,
776
+ )
777
+ ?.address?.toLowerCase();
778
+ }
779
+
780
+ const searchedChainId =
781
+ // currently set chain
782
+ srcChain ||
783
+ // chain set by the integrator
784
+ integrationConfig?.srcChainId ||
785
+ // wallet chain
786
+ (walletChainId ? `${walletChainId}` : undefined) ||
787
+ // default chain
788
+ (bridgeUI ? undefined : DEFAULT_SOURCE_CHAIN_ID);
789
+
790
+ const initSrcChain = supportedChains.find(
791
+ ({ chainId }) => chainId === searchedChainId,
792
+ );
793
+
794
+ let initSrcToken: Token | undefined;
795
+ if (initSrcChain) {
796
+ initSrcToken = initSrcChain.tokens.find(
797
+ ({ address }) =>
798
+ address.toLowerCase() === srcTokenAddress?.toLowerCase(),
799
+ );
800
+ }
801
+
802
+ let initDstChain: Chain | undefined = bridgeUI
752
803
  ? undefined
753
804
  : supportedChains.find(
754
- ({ chainId }) =>
755
- chainId ===
756
- (integrationConfig.dstChainId || DEFAULT_DESTINATION_CHAIN_ID),
805
+ ({ chainId }) => chainId === DEFAULT_DESTINATION_CHAIN_ID,
757
806
  );
758
- setDstChain(initDstChain);
759
807
 
808
+ // if integrator set it, use it
809
+ if (integrationConfig.dstChainId) {
810
+ initDstChain = supportedChains.find(
811
+ ({ chainId }) => chainId === integrationConfig.dstChainId,
812
+ );
813
+ }
814
+
815
+ let initDstToken: Token | undefined;
760
816
  if (initDstChain) {
761
- const initDstToken = initDstChain.tokens.find(
762
- ({ address }) =>
763
- address.toLowerCase() ===
764
- (integrationConfig.dstTokenAddr?.toLowerCase() ||
765
- DEFAULT_DESTINATION_TOKEN_ADDR.toLowerCase()),
817
+ initDstToken = initDstChain.tokens.find(
818
+ ({ address }) => address.toLowerCase() === dstTokenAddress,
766
819
  );
767
- setDstToken(initDstToken);
768
- } else {
769
- setDstToken(undefined);
770
820
  }
771
- }, [integrationConfig, supportedChains, bridgeUI, walletChainId]);
821
+
822
+ // the values here should not be from state but from the code above
823
+ const { source, target } = mapToValidPath({
824
+ source: { chain: initSrcChain, token: initSrcToken },
825
+ target: { chain: initDstChain, token: initDstToken },
826
+ type: bridgeUI ? "BRIDGE" : "SWAP",
827
+ supportedChains,
828
+ bridgeTokensDictionary,
829
+ });
830
+
831
+ setSrcChain(source.chain);
832
+ setDstChain(target.chain);
833
+ setSrcToken(source.token);
834
+ setDstToken(target.token);
835
+ // Mark initial state as set
836
+ setIsInitialStateSet(true);
837
+ // eslint-disable-next-line react-hooks/exhaustive-deps
838
+ }, [
839
+ integrationConfig?.srcChainId,
840
+ integrationConfig?.srcTokenAddr,
841
+ integrationConfig?.dstChainId,
842
+ integrationConfig?.dstTokenAddr,
843
+ supportedChains,
844
+ bridgeUI,
845
+ walletChainId,
846
+ isInitialStateSet,
847
+ bridgeTokensDictionary,
848
+ ]);
772
849
 
773
850
  useEffect(() => {
774
- if (!bridgeTokensDictionary) {
851
+ if (
852
+ !bridgeTokensDictionary ||
853
+ supportedChains.length === 0 ||
854
+ !isInitialStateSet
855
+ ) {
775
856
  return;
776
857
  }
777
858
 
@@ -814,11 +895,12 @@ export const SwapProvider = ({
814
895
  } else if (window.xPayOnDstChainChange) {
815
896
  window.xPayOnDstChainChange(dstChain?.chainId);
816
897
  }
817
- }, [integrationConfig, dstChain]);
898
+ // eslint-disable-next-line react-hooks/exhaustive-deps
899
+ }, [integrationConfig.onDstChainChange, dstChain]);
818
900
 
819
901
  useEffect(() => {
820
902
  const token = dstToken
821
- ? { address: dstToken.address.toLowerCase(), symbol: dstToken.symbol }
903
+ ? { address: dstToken.address.toLowerCase(), symbol: dstToken.tokenId }
822
904
  : undefined;
823
905
 
824
906
  if (integrationConfig.onDstTokenChange) {
@@ -826,7 +908,8 @@ export const SwapProvider = ({
826
908
  } else if (window.xPayOnDstTokenChange) {
827
909
  window.xPayOnDstTokenChange(token);
828
910
  }
829
- }, [integrationConfig, dstToken]);
911
+ // eslint-disable-next-line react-hooks/exhaustive-deps
912
+ }, [integrationConfig.onDstTokenChange, dstToken]);
830
913
 
831
914
  useEffect(() => {
832
915
  if (integrationConfig.onSrcChainChange) {
@@ -834,11 +917,12 @@ export const SwapProvider = ({
834
917
  } else if (window.xPayOnSrcChainChange) {
835
918
  window.xPayOnSrcChainChange(srcChain?.chainId);
836
919
  }
837
- }, [integrationConfig, srcChain]);
920
+ // eslint-disable-next-line react-hooks/exhaustive-deps
921
+ }, [integrationConfig.onSrcChainChange, srcChain]);
838
922
 
839
923
  useEffect(() => {
840
924
  const token = srcToken
841
- ? { address: srcToken.address.toLowerCase(), symbol: srcToken.symbol }
925
+ ? { address: srcToken.address.toLowerCase(), symbol: srcToken.tokenId }
842
926
  : undefined;
843
927
 
844
928
  if (integrationConfig.onSrcTokenChange) {
@@ -846,7 +930,8 @@ export const SwapProvider = ({
846
930
  } else if (window.xPayOnSrcTokenChange) {
847
931
  window.xPayOnSrcTokenChange(token);
848
932
  }
849
- }, [integrationConfig, srcToken]);
933
+ // eslint-disable-next-line react-hooks/exhaustive-deps
934
+ }, [integrationConfig.onSrcTokenChange, srcToken]);
850
935
 
851
936
  return (
852
937
  <swapContext.Provider
@@ -1,4 +1,4 @@
1
- import { Ecosystem, EnqueueTxProps, TxStatus } from "@src/models";
1
+ import { Ecosystem, EnqueueTxProps } from "@src/models";
2
2
  import { ContractReceipt, ContractTransaction } from "ethers";
3
3
  import {
4
4
  Context,
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  DEFAULT_DESTINATION_CHAIN_ID,
3
+ DEFAULT_DESTINATION_TOKEN_ADDR,
3
4
  DEFAULT_SOURCE_CHAIN_ID,
5
+ DEFAULT_SOURCE_TOKEN_ADDR,
4
6
  } from "@src/constants";
5
7
  import { BridgeTokensDictionary, Chain, Token } from "@src/models";
6
8
 
7
- const getDefaultSourceChain = ({
9
+ export const getDefaultSwapSourceChain = ({
8
10
  supportedChains,
9
11
  }: {
10
12
  supportedChains: Chain[];
@@ -23,7 +25,7 @@ const getDefaultSourceChain = ({
23
25
  return defaultSourceChain;
24
26
  };
25
27
 
26
- const getDefaultDestinationChain = ({
28
+ export const getDefaultDestinationChain = ({
27
29
  supportedChains,
28
30
  }: {
29
31
  supportedChains: Chain[];
@@ -42,15 +44,26 @@ const getDefaultDestinationChain = ({
42
44
  return defaultTargetChain;
43
45
  };
44
46
 
45
- const getDefaultSwapTokenForChain = (
47
+ export const getDefaultSwapTokenForChain = (
46
48
  chain: Chain,
49
+ defaultTokenAddress: string,
47
50
  sameChain?: boolean,
48
51
  sourceTokenId?: string,
49
52
  ) => {
50
- const xswapToken = chain.tokens.find((token) => token.tokenId === "XSWAP");
53
+ // First check if there are any supported tokens at all
54
+ const hasAnySupportedTokens = chain.tokens.some((token) => token.supported);
55
+ if (!hasAnySupportedTokens) {
56
+ return undefined;
57
+ }
51
58
 
52
- if (xswapToken) {
53
- return xswapToken;
59
+ const defaultToken = chain.tokens.find(
60
+ (token) =>
61
+ token.address.toLowerCase() === defaultTokenAddress.toLowerCase() &&
62
+ token.supported,
63
+ );
64
+
65
+ if (defaultToken) {
66
+ return defaultToken;
54
67
  }
55
68
 
56
69
  const fallbackToken = chain.tokens.find((token) =>
@@ -63,10 +76,10 @@ const getDefaultSwapTokenForChain = (
63
76
  return fallbackToken;
64
77
  }
65
78
 
66
- return chain.tokens[0];
79
+ return undefined;
67
80
  };
68
81
 
69
- const getDefaultBridgeTokenForChain = ({
82
+ export const getDefaultBridgeTokenForChain = ({
70
83
  chain,
71
84
  bridgeTokensDictionary,
72
85
  }: {
@@ -158,15 +171,17 @@ const mapToValidPathSwap = ({
158
171
  supportedChains,
159
172
  }: Omit<MapToValidPathParams, "type">) => {
160
173
  let newSourceChain: Chain | undefined =
161
- source.chain || getDefaultSourceChain({ supportedChains });
174
+ source.chain || getDefaultSwapSourceChain({ supportedChains });
162
175
  let newTargetChain: Chain | undefined =
163
176
  target.chain || getDefaultDestinationChain({ supportedChains });
164
177
  let newSourceToken: Token | undefined =
165
- source.token || getDefaultSwapTokenForChain(newSourceChain);
178
+ source.token ||
179
+ getDefaultSwapTokenForChain(newSourceChain, DEFAULT_SOURCE_TOKEN_ADDR);
166
180
  let newTargetToken: Token | undefined =
167
181
  target.token ||
168
182
  getDefaultSwapTokenForChain(
169
183
  newTargetChain,
184
+ DEFAULT_DESTINATION_TOKEN_ADDR,
170
185
  newSourceChain.chainId === newTargetChain.chainId,
171
186
  newSourceToken?.tokenId,
172
187
  );
@@ -185,7 +200,7 @@ const mapToValidPathSwap = ({
185
200
  }
186
201
 
187
202
  if (!isSwapSourceValid(newSourceChain)) {
188
- newSourceChain = getDefaultSourceChain({ supportedChains });
203
+ newSourceChain = getDefaultSwapSourceChain({ supportedChains });
189
204
  // We can't have dangling token from a different chain.
190
205
  newSourceToken = undefined;
191
206
 
@@ -242,11 +257,15 @@ const mapToValidPathSwap = ({
242
257
  // Setting tokens
243
258
 
244
259
  if (!newSourceToken) {
245
- newSourceToken = getDefaultSwapTokenForChain(newSourceChain);
260
+ newSourceToken = getDefaultSwapTokenForChain(
261
+ newSourceChain,
262
+ DEFAULT_SOURCE_TOKEN_ADDR,
263
+ );
246
264
  }
247
- if (!newTargetToken) {
265
+ if (!newTargetToken || newSourceToken === newTargetToken) {
248
266
  newTargetToken = getDefaultSwapTokenForChain(
249
267
  newTargetChain,
268
+ DEFAULT_DESTINATION_TOKEN_ADDR,
250
269
  newSourceChain.chainId === newTargetChain.chainId,
251
270
  newSourceToken?.tokenId,
252
271
  );
@@ -270,8 +289,7 @@ const mapToValidPathBridge = ({
270
289
  let newTargetToken = target.token;
271
290
 
272
291
  if (!isBridgeSourceValid(newSourceChain)) {
273
- newSourceChain = getDefaultSourceChain({ supportedChains });
274
- // We can't have dangling token from a different chain.
292
+ newSourceChain = undefined;
275
293
  newSourceToken = undefined;
276
294
 
277
295
  if (!isBridgeSourceValid(newSourceChain)) {
@@ -297,7 +315,7 @@ const mapToValidPathBridge = ({
297
315
  sourceToken: newSourceToken,
298
316
  })
299
317
  ) {
300
- newTargetChain = getDefaultDestinationChain({ supportedChains });
318
+ newTargetChain = undefined;
301
319
  // We can't have dangling token from a different chain.
302
320
  newTargetToken = undefined;
303
321
 
@@ -329,6 +347,18 @@ const mapToValidPathBridge = ({
329
347
 
330
348
  // Setting tokens
331
349
 
350
+ // check if the source token is bridgeable
351
+ if (newSourceToken && newSourceChain && newTargetChain) {
352
+ const isBridgeable =
353
+ !!bridgeTokensDictionary[newSourceChain.chainId]?.[
354
+ newSourceToken.address.toLowerCase()
355
+ ]?.[newTargetChain.chainId];
356
+
357
+ if (!isBridgeable) {
358
+ newSourceToken = undefined;
359
+ }
360
+ }
361
+
332
362
  if (!newSourceToken) {
333
363
  const defaultToken = getDefaultBridgeTokenForChain({
334
364
  chain: newSourceChain,
@@ -378,6 +408,10 @@ const mapToValidPathBridge = ({
378
408
  }
379
409
  }
380
410
 
411
+ if (!newTargetToken) {
412
+ newTargetChain = undefined;
413
+ }
414
+
381
415
  return {
382
416
  source: { chain: newSourceChain, token: newSourceToken },
383
417
  target: { chain: newTargetChain, token: newTargetToken },
@@ -6,6 +6,7 @@ export default {
6
6
  fontFamily: {
7
7
  body: ["'Satoshi-Medium'", "Tahoma", "Verdana", "ui-sans-serif"],
8
8
  sans: ["Satoshi-Medium", "Tahoma", "Verdana", "ui-sans-serif"],
9
+ "sans-bold": ["Satoshi-Bold", "Tahoma", "Verdana", "ui-sans-serif"],
9
10
  },
10
11
  extend: {
11
12
  colors: {