@xswap-link/sdk 0.2.4 → 0.2.5

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.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "JavaScript SDK for XSwap platform",
5
5
  "homepage": "https://github.com/xswap-link/xswap-sdk",
6
6
  "repository": {
@@ -1,4 +1,4 @@
1
- import { FC } from "react";
1
+ import { FC, useMemo } from "react";
2
2
  import { CoinsIcon, TimerIcon, ChevronUpIcon, ChevronDownIcon } from "../icons";
3
3
  import { safeBigNumberFrom, weiToHumanReadable } from "@src/utils";
4
4
  import { Route, Token } from "@src/models";
@@ -27,6 +27,10 @@ export const FeesDetails: FC<Props> = ({
27
27
  feesDetailsShown,
28
28
  setFeesDetailsShown,
29
29
  }) => {
30
+ const isExpressDeliveryPossible = useMemo(() => {
31
+ return expressChecked && !exceedsExpressDeliveryLimit;
32
+ }, [expressChecked, exceedsExpressDeliveryLimit]);
33
+
30
34
  return (
31
35
  <div className="flex flex-col gap-3 globalBorder rounded-lg bg-[rgba(15,15,15,1)] p-2 sm:p-4 mb-1">
32
36
  <div className="flex w-full items-center justify-between text-xs sm:text-sm">
@@ -51,18 +55,18 @@ export const FeesDetails: FC<Props> = ({
51
55
  )}
52
56
  </div>
53
57
  <div className="flex gap-1 items-center">
54
- <div
55
- className={`flex gap-1 items-center font-medium ${
56
- expressChecked && !exceedsExpressDeliveryLimit
57
- ? "text-x_green"
58
- : "text-white opacity-60"
59
- }`}
60
- >
61
- <TimerIcon />
62
- {expressChecked && !exceedsExpressDeliveryLimit
63
- ? "Fast ~ 30sec "
64
- : "Normal ~ 30min"}
65
- </div>
58
+ {route?.xSwapFees.expressDeliveryFee && !isGettingRoute && (
59
+ <div
60
+ className={`flex gap-1 items-center font-medium ${
61
+ isExpressDeliveryPossible
62
+ ? "text-x_green"
63
+ : "text-white opacity-60"
64
+ }`}
65
+ >
66
+ <TimerIcon />
67
+ {isExpressDeliveryPossible ? "Fast ~ 30sec " : "Normal ~ 30min"}
68
+ </div>
69
+ )}
66
70
  <div
67
71
  onClick={() => setFeesDetailsShown((x) => !x)}
68
72
  className="font-medium text-white opacity-60 cursor-pointer flex items-center w-[15px] h-[9px]"
@@ -3,7 +3,7 @@ import React, { FormEvent, useEffect, useState } from "react";
3
3
  import { ethers } from "ethers";
4
4
  import { useAccount, useSwitchChain } from "wagmi";
5
5
  import { Chain, Route, Token, TokenBalances, TokenPrices } from "@src/models";
6
- import { getPrices, getRoute } from "@src/services";
6
+ import { getCustomTokenData, getPrices, getRoute } from "@src/services";
7
7
  import { getBalances } from "@src/utils";
8
8
  import { ROUTE_TIMEOUT_MS } from "@src/constants";
9
9
  import { TxConfigFormProps } from "@src/components";
@@ -21,6 +21,7 @@ import { DEFAULT_SOURCE_CHAIN_ID } from "@src/constants";
21
21
  import { Button } from "./Button";
22
22
 
23
23
  export const Form = ({
24
+ integratorId,
24
25
  dstChainId,
25
26
  dstTokenAddr,
26
27
  customContractCalls,
@@ -77,15 +78,29 @@ export const Form = ({
77
78
  }, [supportedChains, dstChainId]);
78
79
 
79
80
  useEffect(() => {
80
- setDstToken(
81
- dstChain?.tokens.find(
81
+ (async () => {
82
+ if (!dstChain) {
83
+ return;
84
+ }
85
+
86
+ let dstTokenResult = dstChain?.tokens.find(
82
87
  (token) => token.address.toLowerCase() === dstTokenAddr.toLowerCase(),
83
- ),
84
- );
88
+ );
89
+
90
+ if (!dstTokenResult) {
91
+ dstTokenResult = await getCustomTokenData(
92
+ dstChain,
93
+ dstTokenAddr.toLowerCase(),
94
+ );
95
+ }
96
+
97
+ setDstToken(dstTokenResult);
98
+ })();
85
99
  }, [dstChain]);
86
100
 
87
101
  useEffect(() => {
88
102
  if (
103
+ integratorId &&
89
104
  srcChain &&
90
105
  srcToken &&
91
106
  dstChain &&
@@ -104,6 +119,7 @@ export const Form = ({
104
119
  `Configuration error: selected token ($${srcToken.symbol} ${srcToken.name}) has unknown decimals param.`,
105
120
  );
106
121
  getRoute({
122
+ integratorId,
107
123
  fromAmount: ethers.utils
108
124
  .parseUnits(debouncedAmount, srcToken.decimals)
109
125
  .toString(),
@@ -21,15 +21,42 @@ export const History: FC<Props> = ({ signer, supportedChains }) => {
21
21
  const fetchedTransactions: Transaction[] = [];
22
22
  if (downloadedHistory) {
23
23
  for (const entry of downloadedHistory.history) {
24
+ let status: TransactionStatus = "IN_PROGRESS";
25
+
26
+ // single chain tx
24
27
  if (!entry.source) {
25
28
  continue;
26
29
  }
27
- let status: TransactionStatus;
28
- if (entry.failed) status = "REVERTED";
30
+
31
+ if (entry.failed) {
32
+ status = "REVERTED";
33
+ }
29
34
  if (!!entry.target) {
30
35
  status = "DONE";
31
- } else {
32
- status = "IN_PROGRESS";
36
+ }
37
+
38
+ if (entry.transferType === "SINGLE_CHAIN") {
39
+ if (!entry.target) {
40
+ continue;
41
+ }
42
+
43
+ const blockTimestampMs = new Date(entry.target.blockTime).getTime();
44
+ const estimatedDeliveryTimestamp = blockTimestampMs / 1000;
45
+
46
+ fetchedTransactions.push({
47
+ hash: entry.target.transactionHash,
48
+ timestamp: blockTimestampMs / 1000,
49
+ sourceChainId: entry.target.blockchainId,
50
+ targetChainId: entry.target.blockchainId,
51
+ amountWei: entry.source.tokenAmount,
52
+ tokenAddress: entry.source.tokenAddress,
53
+ tokenOutAddress: entry.target?.tokenAmount,
54
+ tokenOutAmount: entry.target?.tokenAmount,
55
+ estimatedDeliveryTimestamp,
56
+ status,
57
+ });
58
+
59
+ continue;
33
60
  }
34
61
 
35
62
  const blockTimestampMs = new Date(entry.source.blockTime).getTime();
@@ -1,10 +1,11 @@
1
- import React, { FC, useEffect, useMemo, useState } from "react";
1
+ import { FC, useEffect, useMemo, useState } from "react";
2
2
  import { weiToHumanReadable } from "@src/utils";
3
3
  import { Chain, Route, Token, TokenPrices } from "@src/models";
4
4
  import { ArrowDownIcon } from "../icons";
5
5
  import { UsdPrice } from "@src/components/TxConfigForm/UsdPrice";
6
6
  import { getPrices } from "@src/services";
7
7
  import { Skeleton } from "@src/components";
8
+ import { UnknownTokenLogo } from "../UnknownTokenLogo/UnknownTokenLogo";
8
9
 
9
10
  interface Props {
10
11
  isGettingRoute: boolean;
@@ -58,10 +59,21 @@ export const Summary: FC<Props> = ({
58
59
  </div>
59
60
  <div className="flex flex-col items-end gap-1 text-sm">
60
61
  <div className="flex justify-center items-center gap-1">
61
- <div className="w-4 sm:w-6 h-4 sm:h-6">
62
- <img src={dstToken?.image} alt={dstToken?.name} />
62
+ <div className="w-5 h-5">
63
+ {dstToken?.image ? (
64
+ <img
65
+ className="w-5 h-5"
66
+ src={dstToken?.image}
67
+ alt={dstToken?.name}
68
+ />
69
+ ) : (
70
+ <UnknownTokenLogo
71
+ tokenName={"?"}
72
+ className="token-select__generated-logo"
73
+ />
74
+ )}
63
75
  </div>
64
- <p className="text-base sm:text-xl">{dstToken?.symbol}</p>
76
+ <p className="text-base sm:text-xl">{dstToken?.name}</p>
65
77
  </div>
66
78
  <div className="flex justify-center items-center gap-1">
67
79
  <p className="text-[rgba(255,255,255,0.6)] text-right">on </p>
@@ -63,7 +63,7 @@ export const SwapPanel: FC<Props> = ({
63
63
  weiToHumanReadable({
64
64
  amount: balances[srcToken.address]?.toString() || "0",
65
65
  decimals: srcToken.decimals,
66
- precisionFractionalPlaces: 4,
66
+ precisionFractionalPlaces: srcToken.decimals,
67
67
  }),
68
68
  );
69
69
  };
@@ -8,6 +8,7 @@ import { PoweredBy } from "@src/components/TxConfigForm/PoweredBy";
8
8
  import { Form } from "./Form";
9
9
 
10
10
  export type TxConfigFormProps = {
11
+ integratorId: string;
11
12
  dstChainId: string;
12
13
  dstTokenAddr: string;
13
14
  customContractCalls: ContractCall[];
@@ -16,6 +17,7 @@ export type TxConfigFormProps = {
16
17
  };
17
18
 
18
19
  const TxConfigForm = ({
20
+ integratorId,
19
21
  dstChainId,
20
22
  dstTokenAddr,
21
23
  customContractCalls,
@@ -50,6 +52,7 @@ const TxConfigForm = ({
50
52
  >
51
53
  <div className="relative bg-black rounded-3xl overflow-auto text-white border-2 border-solid border-[rgba(255,255,255,0.1)]">
52
54
  <Form
55
+ integratorId={integratorId}
53
56
  dstChainId={dstChainId}
54
57
  dstTokenAddr={dstTokenAddr}
55
58
  customContractCalls={customContractCalls}
@@ -67,6 +70,7 @@ const TxConfigForm = ({
67
70
  };
68
71
 
69
72
  export const openTxConfigForm = async ({
73
+ integratorId,
70
74
  dstChainId,
71
75
  dstTokenAddr,
72
76
  customContractCalls,
@@ -78,6 +82,7 @@ export const openTxConfigForm = async ({
78
82
  await waitForInitialization();
79
83
  xswapRoot.render(
80
84
  <TxConfigForm
85
+ integratorId={integratorId}
81
86
  dstChainId={dstChainId}
82
87
  dstTokenAddr={dstTokenAddr}
83
88
  customContractCalls={customContractCalls}
@@ -9,6 +9,7 @@ import {
9
9
  } from "../icons";
10
10
  import { MonitoredTransaction } from "@src/models";
11
11
  import { txStatusRoot } from "@src/config";
12
+ import { UnknownTokenLogo } from "../UnknownTokenLogo/UnknownTokenLogo";
12
13
 
13
14
  interface Props {
14
15
  transaction: MonitoredTransaction;
@@ -16,15 +17,14 @@ interface Props {
16
17
 
17
18
  export const TxStatusButton: FC<Props> = ({ transaction }) => {
18
19
  const {
19
- fromChain,
20
- fromChainImage,
21
- fromToken,
22
- fromTokenImage,
23
- fromAmount,
24
- toChain,
25
- toChainImage,
20
+ srcChain: fromChain,
21
+ srcChainImage: fromChainImage,
22
+ srcToken: fromToken,
23
+ srcTokenImage: fromTokenImage,
24
+ srcAmount: fromAmount,
25
+ dstChain: toChain,
26
+ dstChainImage: toChainImage,
26
27
  isDone,
27
- txHash,
28
28
  explorer,
29
29
  } = transaction;
30
30
  const [isWide, setIsWide] = useState(false);
@@ -70,11 +70,19 @@ export const TxStatusButton: FC<Props> = ({ transaction }) => {
70
70
  <div>Sent</div>
71
71
  <div>{fromAmount}</div>
72
72
  <div>{fromToken}</div>
73
- <img
74
- className="w-5 h-5"
75
- src={fromTokenImage}
76
- alt="source token"
77
- />
73
+
74
+ {fromTokenImage ? (
75
+ <img
76
+ className="w-5 h-5"
77
+ src={fromTokenImage}
78
+ alt="source token"
79
+ />
80
+ ) : (
81
+ <UnknownTokenLogo
82
+ tokenName={"?"}
83
+ className="token-select__generated-logo"
84
+ />
85
+ )}
78
86
  </div>
79
87
  </div>
80
88
  <a
@@ -0,0 +1,14 @@
1
+ interface Props {
2
+ tokenName: string;
3
+ className?: string;
4
+ }
5
+
6
+ export const UnknownTokenLogo = ({ tokenName, className }: Props) => {
7
+ return (
8
+ <div
9
+ className={`w-5 h-5 rounded-full bg-white shadow-sm flex items-center justify-center text-black ${className}`}
10
+ >
11
+ {tokenName.substring(0, 1)}
12
+ </div>
13
+ );
14
+ };
@@ -30,5 +30,5 @@
30
30
  }
31
31
 
32
32
  .xswap-tx-status {
33
- @apply absolute bottom-5 right-2 flex flex-col gap-3 pl-9 items-end w-full z-50;
33
+ @apply fixed bottom-5 right-2 flex flex-col gap-3 pl-9 items-end w-full z-50;
34
34
  }
@@ -17,7 +17,7 @@ export const initDocument = () => {
17
17
 
18
18
  Promise.all([
19
19
  createStyleElement(),
20
- createXswapRoot(),
20
+ createXSwapRoot(),
21
21
  createTxStatusRoot(),
22
22
  ]).then(() => {
23
23
  initCompleted = true;
@@ -46,7 +46,7 @@ const createStyleElement = async () => {
46
46
  document.body.appendChild(style);
47
47
  };
48
48
 
49
- const createXswapRoot = async () => {
49
+ const createXSwapRoot = async () => {
50
50
  const xswapElement = document.createElement("div");
51
51
  xswapElement.setAttribute("id", "xswap-modal");
52
52
  document.body.appendChild(xswapElement);
package/src/index.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { initDocument } from "@src/config/init";
2
+ import { openTransactionModal, renderTxStatus } from "@src/services";
2
3
 
3
4
  initDocument();
4
5
 
5
6
  export * from "@src/models";
6
- export { getSwapTx, monitorTransactionStatus } from "@src/services";
7
+ export { openTransactionModal, renderTxStatus } from "@src/services";
8
+
9
+ const XPay = {
10
+ openTransactionModal,
11
+ renderTxStatus,
12
+ };
13
+
14
+ export default XPay;
@@ -24,7 +24,7 @@ export type Token = {
24
24
  name: string;
25
25
  symbol: string;
26
26
  decimals: number;
27
- image: string;
27
+ image?: string;
28
28
  priority: number;
29
29
  quickPick?: boolean;
30
30
  supported?: boolean;
@@ -34,7 +34,11 @@ export type HistoryTransaction = {
34
34
  } | null;
35
35
  };
36
36
 
37
- export type TransactionStatus = "IN_PROGRESS" | "DONE" | "REVERTED";
37
+ export type TransactionStatus =
38
+ | "IN_PROGRESS"
39
+ | "DONE"
40
+ | "REVERTED"
41
+ | "NOT_FOUND";
38
42
 
39
43
  export type Transaction = {
40
44
  hash: string;
@@ -50,13 +54,13 @@ export type Transaction = {
50
54
  };
51
55
 
52
56
  export type MonitoredTransaction = {
53
- fromChain: string | undefined;
54
- fromChainImage: string | undefined;
55
- fromToken: string | undefined;
56
- fromTokenImage: string | undefined;
57
- fromAmount: string | undefined;
58
- toChain: string | undefined;
59
- toChainImage: string | undefined;
57
+ srcChain: string | undefined;
58
+ srcChainImage: string | undefined;
59
+ srcToken: string | undefined;
60
+ srcTokenImage: string | undefined;
61
+ srcAmount: string | undefined;
62
+ dstChain: string | undefined;
63
+ dstChainImage: string | undefined;
60
64
  isDone: boolean;
61
65
  txHash: string;
62
66
  explorer: string | undefined;
@@ -1,6 +1,7 @@
1
1
  import { CoinTypeAddress, CollectFees, ContractCall } from "@src/models";
2
2
 
3
3
  export type GetRoutePayload = {
4
+ integratorId: string;
4
5
  fromChain: string;
5
6
  toChain: string;
6
7
  fromToken: string;
@@ -1,6 +1,7 @@
1
1
  import { ContractCall } from "@src/models";
2
2
 
3
3
  export type GetSwapTxPayload = {
4
+ integratorId: string;
4
5
  dstChain: string;
5
6
  dstToken: string;
6
7
  customContractCalls?: ContractCall[];
@@ -9,6 +9,7 @@ import {
9
9
  Token,
10
10
  TokenPrices,
11
11
  TransactionHistory,
12
+ TransactionStatus,
12
13
  } from "@src/models";
13
14
 
14
15
  async function _sendRequest<T>(
@@ -110,3 +111,11 @@ export async function getPrices(payload: GetPricesPayload) {
110
111
  })}`,
111
112
  );
112
113
  }
114
+
115
+ export async function getTxStatus(payload: { transferId: string }) {
116
+ return await _sendRequest<{ status: TransactionStatus }>(
117
+ `/getTxStatus?${new URLSearchParams({
118
+ data: JSON.stringify(payload), // todo remove once gcp starts working
119
+ })}`,
120
+ );
121
+ }
@@ -0,0 +1,49 @@
1
+ import { ERC20Abi } from "@src/contracts";
2
+ import { ethers } from "ethers";
3
+ import { Chain, Token } from "@src/models";
4
+
5
+ export const getCustomTokenData = async (
6
+ chain: Chain,
7
+ tokenAddress: string,
8
+ ): Promise<Token> => {
9
+ const rpcUrl = chain.publicRpcUrls[0];
10
+
11
+ const customTokenContract = new ethers.Contract(
12
+ tokenAddress.toLowerCase(),
13
+ ERC20Abi,
14
+ ethers.getDefaultProvider(rpcUrl),
15
+ );
16
+ try {
17
+ const query = async (
18
+ contract: ethers.Contract | null,
19
+ method: string,
20
+ args: unknown[] = [],
21
+ ) => {
22
+ if (contract?.provider) {
23
+ const response = await contract[method](...args);
24
+ return response;
25
+ }
26
+ return null;
27
+ };
28
+ const responses = await Promise.all([
29
+ query(customTokenContract, "decimals"),
30
+ query(customTokenContract, "name"),
31
+ query(customTokenContract, "symbol"),
32
+ ]);
33
+ const [decimals, name, symbol] = responses;
34
+
35
+ return {
36
+ address: tokenAddress,
37
+ symbol: symbol,
38
+ name: name,
39
+ decimals: decimals,
40
+ quickPick: false,
41
+ supported: true,
42
+ priority: 0,
43
+ };
44
+ } catch (_) {
45
+ throw new Error(
46
+ `Provided token ${tokenAddress} does not exists on chain ${chain.displayName}`,
47
+ );
48
+ }
49
+ };
@@ -1,2 +1,3 @@
1
1
  export * from "./api";
2
2
  export * from "./integrations";
3
+ export * from "./blockchain";
@@ -1,5 +1,5 @@
1
1
  import { Chain, MonitoredTransaction, Web3Environment } from "@src/models";
2
- import { getChains } from "@src/services";
2
+ import { getChains, getTxStatus } from "@src/services";
3
3
  import { ethers } from "ethers";
4
4
  import { weiToHumanReadable } from "@src/utils";
5
5
  import {
@@ -18,8 +18,9 @@ import {
18
18
  } from "@src/components";
19
19
  import { ADDRESSES } from "@src/contracts";
20
20
  import retry from "async-retry";
21
+ import { getCustomTokenData } from "../blockchain";
21
22
 
22
- export const monitorTransactionStatus = async (
23
+ export const renderTxStatus = async (
23
24
  txChainId: string,
24
25
  txHash: string,
25
26
  ): Promise<void> => {
@@ -52,11 +53,20 @@ export const monitorTransactionStatus = async (
52
53
  const srcChain = supportedChains.find(
53
54
  (chain) => chain.chainId === txChainId.toString(),
54
55
  );
56
+ if (!srcChain) {
57
+ throw new Error(`Unknown src chain ${txChainId}`);
58
+ }
59
+
60
+ const tokenAddress = decodedMessageSentEvent.args?.token.toString();
55
61
 
56
- const srcToken = srcChain?.tokens.find(
62
+ let srcToken = srcChain.tokens.find(
57
63
  (token) => token.address === decodedMessageSentEvent.args?.token.toString(),
58
64
  );
59
65
 
66
+ if (!srcToken) {
67
+ srcToken = await getCustomTokenData(srcChain, tokenAddress);
68
+ }
69
+
60
70
  const dstChain = supportedChains.find(
61
71
  (chain) =>
62
72
  chain.ccipChainId ===
@@ -69,19 +79,21 @@ export const monitorTransactionStatus = async (
69
79
  throw new Error(`Unknown destination chain!`);
70
80
  }
71
81
 
82
+ const tokenAmount = weiToHumanReadable({
83
+ amount: decodedMessageSentEvent.args?.tokenAmount.toString(),
84
+ decimals: srcToken?.decimals || 18,
85
+ precisionFractionalPlaces: 4,
86
+ });
87
+
72
88
  const transaction: MonitoredTransaction = {
73
89
  txHash: txHash,
74
- fromChain: srcChain?.displayName,
75
- fromChainImage: srcChain?.image,
76
- toChain: dstChain.displayName,
77
- toChainImage: dstChain.image,
78
- fromToken: srcToken?.symbol,
79
- fromTokenImage: srcToken?.image,
80
- fromAmount: weiToHumanReadable({
81
- amount: decodedMessageSentEvent.args?.tokenAmount.toString(),
82
- decimals: srcToken?.decimals || 18,
83
- precisionFractionalPlaces: 4,
84
- }),
90
+ srcChain: srcChain.displayName,
91
+ srcChainImage: srcChain.image,
92
+ dstChain: dstChain.displayName,
93
+ dstChainImage: dstChain.image,
94
+ srcToken: srcToken?.symbol,
95
+ srcTokenImage: srcToken?.image,
96
+ srcAmount: tokenAmount === "0" ? "<0.0001" : tokenAmount,
85
97
  isDone: false,
86
98
  explorer: `${CCIP_EXPLORER}/tx/${txHash}`,
87
99
  };
@@ -120,6 +132,14 @@ export const monitorTransactionStatus = async (
120
132
  removeTransactionFromRenderedTransactions(txHash);
121
133
  renderTxStatusButtons();
122
134
  };
135
+
136
+ // check tx status on backend
137
+ getTxStatus({ transferId: messageId }).then((response) => {
138
+ if (response.status === "DONE") {
139
+ onSuccess();
140
+ }
141
+ });
142
+
123
143
  xSwapRouterOnDestination.once(msgReceivedEventFilter, () => onSuccess());
124
144
  };
125
145
 
@@ -6,8 +6,10 @@ import {
6
6
  } from "@src/models";
7
7
  import { getChains } from "@src/services";
8
8
  import { openTxConfigForm } from "@src/components";
9
+ import { isETHAddressValid } from "@src/utils";
9
10
 
10
- export const getSwapTx = async ({
11
+ export const openTransactionModal = async ({
12
+ integratorId,
11
13
  dstChain,
12
14
  dstToken,
13
15
  customContractCalls = [],
@@ -21,6 +23,7 @@ export const getSwapTx = async ({
21
23
  validateSwapTxData(supportedChains, dstChain, dstToken);
22
24
 
23
25
  const route = await openTxConfigForm({
26
+ integratorId: integratorId,
24
27
  dstChainId: dstChain,
25
28
  dstTokenAddr: dstToken,
26
29
  customContractCalls,
@@ -43,7 +46,9 @@ const validateSwapTxData = (
43
46
  throw new Error(`Provided chain '${dstChain}' is not supported`);
44
47
  }
45
48
 
46
- if (!supportedDstChain.tokens.some(({ address }) => address === dstToken)) {
47
- throw new Error(`Provided token '${dstToken}' is not supported`);
49
+ if (!isETHAddressValid(dstToken)) {
50
+ throw new Error(
51
+ `Provided token address ${dstToken} on chain ${dstChain} is not correct`,
52
+ );
48
53
  }
49
54
  };
@@ -4,3 +4,7 @@ export const shortAddress = (address: string): string => {
4
4
  address.length,
5
5
  )}`;
6
6
  };
7
+
8
+ export const isETHAddressValid = (address: string): boolean => {
9
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
10
+ };