@xswap-link/sdk 0.13.1 → 0.14.1
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/CHANGELOG.md +22 -0
- package/dist/index.d.mts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.global.js +87 -86
- package/dist/index.js +2888 -2521
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2828 -2458
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +5 -3
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +16 -0
- package/src/assets/icons/CaretDownIcon.tsx +12 -0
- package/src/assets/icons/CaretsUpDownIcon.tsx +13 -0
- package/src/assets/icons/ChainlinkMarkIcon.tsx +17 -0
- package/src/assets/icons/ClockIcon.tsx +14 -0
- package/src/assets/icons/CloseIcon.tsx +6 -2
- package/src/assets/icons/WarningIcon.tsx +19 -0
- package/src/assets/icons/index.ts +6 -0
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +12 -5
- package/src/components/Swap/Header/Controls/index.tsx +50 -36
- package/src/components/Swap/Header/index.tsx +12 -5
- package/src/components/Swap/HistoryView/ActivityCard/index.tsx +212 -0
- package/src/components/Swap/HistoryView/index.tsx +24 -6
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +5 -7
- package/src/components/Swap/SettingsView/Delivery/index.tsx +27 -28
- package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +27 -24
- package/src/components/Swap/SettingsView/Slippage/index.tsx +44 -43
- package/src/components/Swap/SettingsView/index.tsx +1 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +50 -4
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +9 -19
- package/src/components/Swap/SwapView/FeesPanel/Fee/index.tsx +4 -4
- package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +5 -9
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +21 -22
- package/src/components/Swap/SwapView/SwapButton/index.tsx +25 -6
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +56 -12
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +42 -41
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +159 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +105 -55
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +226 -276
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +57 -97
- package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +27 -0
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +60 -49
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +20 -25
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +6 -1
- package/src/components/Swap/SwapView/index.tsx +2 -1
- package/src/components/Swap/index.tsx +20 -4
- package/src/components/ToggleButton/index.tsx +18 -15
- package/src/components/TxConfigForm/index.tsx +5 -5
- package/src/components/TxWidgetWC/index.tsx +4 -0
- package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
- package/src/context/HistoryProvider.tsx +2 -1
- package/src/context/SwapProvider.tsx +87 -62
- package/src/context/TxUIWrapper.tsx +3 -3
- package/src/hooks/useEvmContractApi.ts +28 -4
- package/src/models/TransactionHistory.ts +1 -0
- package/src/models/payloads/GetBalancesPayload.ts +1 -0
- package/src/models/payloads/ModalIntegrationPayload.ts +1 -0
- package/src/models/payloads/WidgetIntegrationPayload.ts +1 -0
- package/src/services/api.ts +37 -0
- package/src/utils/index.ts +48 -1
- package/src/utils/tokens.ts +20 -1
- package/tailwind.config.js +4 -1
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +0 -97
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/index.tsx +0 -103
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/index.tsx +0 -65
- package/src/components/Swap/SwapView/SwapPanel/MaxPanel/index.tsx +0 -43
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/QuickPickTokenItem/index.tsx +0 -37
|
@@ -28,10 +28,11 @@ import {
|
|
|
28
28
|
getPrices,
|
|
29
29
|
getRoute,
|
|
30
30
|
getSolanaRoute,
|
|
31
|
+
getTokenBalance,
|
|
31
32
|
} from "@src/services";
|
|
33
|
+
import { getCustomTokenRefs } from "@src/utils/tokens";
|
|
32
34
|
import {
|
|
33
35
|
deepMergeObjects,
|
|
34
|
-
getBalanceOf,
|
|
35
36
|
humanReadableToWei,
|
|
36
37
|
isServer,
|
|
37
38
|
resolveTokenAddress,
|
|
@@ -129,6 +130,10 @@ type SwapContext = {
|
|
|
129
130
|
chainId: string | undefined,
|
|
130
131
|
token: TokenOption,
|
|
131
132
|
) => void;
|
|
133
|
+
// Which side's token/network picker is open. The picker renders in place
|
|
134
|
+
// of the swap view (desktop) or as a bottom sheet (mobile).
|
|
135
|
+
activePicker: "source" | "destination" | null;
|
|
136
|
+
setActivePicker: Dispatch<SetStateAction<"source" | "destination" | null>>;
|
|
132
137
|
};
|
|
133
138
|
|
|
134
139
|
const functionNotImplemented = () => {
|
|
@@ -142,6 +147,8 @@ const init: SwapContext = {
|
|
|
142
147
|
tab: "Swap",
|
|
143
148
|
overlay: false,
|
|
144
149
|
error: "",
|
|
150
|
+
activePicker: null,
|
|
151
|
+
setActivePicker: functionNotImplemented,
|
|
145
152
|
feeToken: undefined,
|
|
146
153
|
srcChain: undefined,
|
|
147
154
|
srcChainTokensOptions: [],
|
|
@@ -224,6 +231,7 @@ export const SwapProvider = ({
|
|
|
224
231
|
// Settings
|
|
225
232
|
const [tab, setTab] = useState(init.tab);
|
|
226
233
|
const [error, setError] = useState(init.error);
|
|
234
|
+
const [activePicker, setActivePicker] = useState(init.activePicker);
|
|
227
235
|
const [expressDelivery, setExpressDelivery] = useState(init.expressDelivery);
|
|
228
236
|
const [slippage, setSlippage] = useState(init.slippage);
|
|
229
237
|
const [infiniteApproval, setInfiniteApproval] = useState(
|
|
@@ -240,6 +248,9 @@ export const SwapProvider = ({
|
|
|
240
248
|
const [tokenBalances, setTokenBalances] = useState<
|
|
241
249
|
Record<string, Record<string, string>>
|
|
242
250
|
>({});
|
|
251
|
+
// bumped when the user imports a token, so the supported tokens mapping and
|
|
252
|
+
// the balances list get rebuilt with the new custom token included
|
|
253
|
+
const [customTokensVersion, setCustomTokensVersion] = useState(0);
|
|
243
254
|
const [bridgeTokens, setBridgeTokens] = useState<BridgeToken[]>([]);
|
|
244
255
|
// Values
|
|
245
256
|
const [srcValue, setSrcValue] = useState(init.srcValue);
|
|
@@ -334,8 +345,9 @@ export const SwapProvider = ({
|
|
|
334
345
|
});
|
|
335
346
|
});
|
|
336
347
|
return deepMergeObjects(chainIdToTokenAddressToData, customTokens);
|
|
348
|
+
// customTokensVersion re-reads localStorage after an in-session import
|
|
337
349
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
-
}, [supportedChains]);
|
|
350
|
+
}, [supportedChains, customTokensVersion]);
|
|
339
351
|
|
|
340
352
|
// useMemo: creates a mapping from each chain ID to available token options sorted by token priority.
|
|
341
353
|
const chainToTokenOptionsMap = useMemo(() => {
|
|
@@ -686,68 +698,70 @@ export const SwapProvider = ({
|
|
|
686
698
|
refreshSelectedTokenBalanceNonce.current = nonce;
|
|
687
699
|
setIsFetchingBalance(true);
|
|
688
700
|
let balance = "0";
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
amount: balanceUI,
|
|
700
|
-
decimals: srcToken.decimals,
|
|
701
|
-
});
|
|
702
|
-
if (options?.forceRpc && balanceFromBackend !== undefined) {
|
|
703
|
-
// keep the backend cache in sync so token pickers and future
|
|
704
|
-
// non-forced refreshes see the fresh value
|
|
705
|
-
const chainId = srcChain.chainId;
|
|
706
|
-
const tokenAddress = srcToken.address;
|
|
707
|
-
const freshBalance = balance;
|
|
708
|
-
setTokenBalances((prev) => ({
|
|
709
|
-
...prev,
|
|
710
|
-
[chainId]: {
|
|
711
|
-
...prev[chainId],
|
|
712
|
-
[tokenAddress]: freshBalance,
|
|
713
|
-
},
|
|
701
|
+
try {
|
|
702
|
+
if (srcChain?.ecosystem === Ecosystem.EVM && evmAddress) {
|
|
703
|
+
const balanceFromBackend =
|
|
704
|
+
tokenBalances[srcChain?.chainId]?.[srcToken.address];
|
|
705
|
+
if (options?.forceRpc || balanceFromBackend === undefined) {
|
|
706
|
+
// single-token read resolved server-side through private RPCs
|
|
707
|
+
({ balance } = await getTokenBalance({
|
|
708
|
+
chainId: srcChain.chainId,
|
|
709
|
+
tokenAddress: srcToken.address,
|
|
710
|
+
walletAddress: evmAddress,
|
|
714
711
|
}));
|
|
712
|
+
if (options?.forceRpc && balanceFromBackend !== undefined) {
|
|
713
|
+
// keep the backend cache in sync so token pickers and future
|
|
714
|
+
// non-forced refreshes see the fresh value
|
|
715
|
+
const chainId = srcChain.chainId;
|
|
716
|
+
const tokenAddress = srcToken.address;
|
|
717
|
+
const freshBalance = balance;
|
|
718
|
+
setTokenBalances((prev) => ({
|
|
719
|
+
...prev,
|
|
720
|
+
[chainId]: {
|
|
721
|
+
...prev[chainId],
|
|
722
|
+
[tokenAddress]: freshBalance,
|
|
723
|
+
},
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
balance = balanceFromBackend;
|
|
715
728
|
}
|
|
716
|
-
} else {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
balance = new BigNumberJS(
|
|
724
|
-
await connection.getBalance(solanaPublicKey),
|
|
725
|
-
).toString();
|
|
726
|
-
} else {
|
|
727
|
-
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
|
|
728
|
-
solanaPublicKey,
|
|
729
|
-
{
|
|
730
|
-
programId: SOLANA_TOKEN_PROGRAM_ID,
|
|
731
|
-
},
|
|
732
|
-
);
|
|
733
|
-
const token2022Accounts =
|
|
734
|
-
await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
|
|
735
|
-
programId: SOLANA_TOKEN_2022_PROGRAM_ID,
|
|
736
|
-
});
|
|
737
|
-
const allTokenAccount = [
|
|
738
|
-
...tokenAccounts.value,
|
|
739
|
-
...token2022Accounts.value,
|
|
740
|
-
];
|
|
741
|
-
const tokenAccount = allTokenAccount.find(
|
|
742
|
-
(account) =>
|
|
743
|
-
account.account.data.parsed.info.mint === srcToken.address,
|
|
744
|
-
);
|
|
745
|
-
if (!tokenAccount) {
|
|
746
|
-
balance = "0";
|
|
729
|
+
} else if (srcChain?.ecosystem === Ecosystem.SOLANA && solanaAddress) {
|
|
730
|
+
const solanaPublicKey = new PublicKey(solanaAddress);
|
|
731
|
+
const connection = new Connection(srcChain.publicRpcUrls[0]!);
|
|
732
|
+
if (srcToken.address === SOLANA_NATIVE_TOKEN_ADDRESS.toBase58()) {
|
|
733
|
+
balance = new BigNumberJS(
|
|
734
|
+
await connection.getBalance(solanaPublicKey),
|
|
735
|
+
).toString();
|
|
747
736
|
} else {
|
|
748
|
-
|
|
737
|
+
const tokenAccounts =
|
|
738
|
+
await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
|
|
739
|
+
programId: SOLANA_TOKEN_PROGRAM_ID,
|
|
740
|
+
});
|
|
741
|
+
const token2022Accounts =
|
|
742
|
+
await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
|
|
743
|
+
programId: SOLANA_TOKEN_2022_PROGRAM_ID,
|
|
744
|
+
});
|
|
745
|
+
const allTokenAccount = [
|
|
746
|
+
...tokenAccounts.value,
|
|
747
|
+
...token2022Accounts.value,
|
|
748
|
+
];
|
|
749
|
+
const tokenAccount = allTokenAccount.find(
|
|
750
|
+
(account) =>
|
|
751
|
+
account.account.data.parsed.info.mint === srcToken.address,
|
|
752
|
+
);
|
|
753
|
+
if (!tokenAccount) {
|
|
754
|
+
balance = "0";
|
|
755
|
+
} else {
|
|
756
|
+
balance =
|
|
757
|
+
tokenAccount.account.data.parsed.info.tokenAmount.amount;
|
|
758
|
+
}
|
|
749
759
|
}
|
|
750
760
|
}
|
|
761
|
+
} catch (_) {
|
|
762
|
+
// failed read — keep the loader on: the balance must be loaded before
|
|
763
|
+
// a swap is allowed, so we never show a stale value as loaded
|
|
764
|
+
return;
|
|
751
765
|
}
|
|
752
766
|
if (nonce !== refreshSelectedTokenBalanceNonce.current) {
|
|
753
767
|
return;
|
|
@@ -790,6 +804,10 @@ export const SwapProvider = ({
|
|
|
790
804
|
? chain
|
|
791
805
|
: { ...chain, tokens: [...chain.tokens, token] };
|
|
792
806
|
|
|
807
|
+
if (!tokenExists) {
|
|
808
|
+
setCustomTokensVersion((version) => version + 1);
|
|
809
|
+
}
|
|
810
|
+
|
|
793
811
|
setSrcChain(updated);
|
|
794
812
|
setSrcToken(token);
|
|
795
813
|
},
|
|
@@ -821,8 +839,9 @@ export const SwapProvider = ({
|
|
|
821
839
|
quoteRoute();
|
|
822
840
|
}, [quoteRoute]);
|
|
823
841
|
// calls refreshBalance to update source token balance on mount or when refreshBalance changes.
|
|
824
|
-
// when the connected wallet changed, force
|
|
825
|
-
// balances still
|
|
842
|
+
// when the connected wallet changed, force a fresh single-token read — the
|
|
843
|
+
// cached balances list still belongs to the previous wallet until
|
|
844
|
+
// getBalances resolves.
|
|
826
845
|
useEffect(() => {
|
|
827
846
|
const walletAddresses = `${evmAddress ?? ""}:${solanaAddress ?? ""}`;
|
|
828
847
|
const walletChanged =
|
|
@@ -832,6 +851,8 @@ export const SwapProvider = ({
|
|
|
832
851
|
refreshBalance(walletChanged ? { forceRpc: true } : undefined);
|
|
833
852
|
}, [refreshBalance, evmAddress, solanaAddress]);
|
|
834
853
|
// fetches user all token balances when a wallet address is available and supported chains are loaded.
|
|
854
|
+
// imported tokens from localStorage are passed along so their balances get
|
|
855
|
+
// resolved server-side too.
|
|
835
856
|
useEffect(() => {
|
|
836
857
|
let isCancelled = false;
|
|
837
858
|
if ((evmAddress || solanaAddress) && supportedChains.length > 0) {
|
|
@@ -839,6 +860,7 @@ export const SwapProvider = ({
|
|
|
839
860
|
getBalances({
|
|
840
861
|
walletAddress: evmAddress,
|
|
841
862
|
solanaWalletAddress: solanaAddress,
|
|
863
|
+
customTokens: getCustomTokenRefs(),
|
|
842
864
|
}).then((balances) => {
|
|
843
865
|
if (!isCancelled) {
|
|
844
866
|
setTokenBalances(balances);
|
|
@@ -850,7 +872,7 @@ export const SwapProvider = ({
|
|
|
850
872
|
return () => {
|
|
851
873
|
isCancelled = true; // prevent state update from stale async call
|
|
852
874
|
};
|
|
853
|
-
}, [evmAddress, solanaAddress, supportedChains.length]);
|
|
875
|
+
}, [evmAddress, solanaAddress, supportedChains.length, customTokensVersion]);
|
|
854
876
|
// sets the fee token from the source chain's tokens by selecting the native token (AddressZero).
|
|
855
877
|
useEffect(() => {
|
|
856
878
|
if (srcChain?.chainId === "mainnet-beta") {
|
|
@@ -1079,6 +1101,7 @@ export const SwapProvider = ({
|
|
|
1079
1101
|
return;
|
|
1080
1102
|
}
|
|
1081
1103
|
|
|
1104
|
+
setActivePicker(null);
|
|
1082
1105
|
setSrcToken(undefined);
|
|
1083
1106
|
setDstToken(undefined);
|
|
1084
1107
|
setSrcChain(undefined);
|
|
@@ -1337,6 +1360,8 @@ export const SwapProvider = ({
|
|
|
1337
1360
|
onConnectWallet,
|
|
1338
1361
|
hasOnConnectWalletCallback,
|
|
1339
1362
|
addTokenToChainIfNotExists,
|
|
1363
|
+
activePicker,
|
|
1364
|
+
setActivePicker,
|
|
1340
1365
|
}}
|
|
1341
1366
|
>
|
|
1342
1367
|
{children}
|
|
@@ -113,7 +113,7 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
113
113
|
</Button>
|
|
114
114
|
|
|
115
115
|
<div
|
|
116
|
-
className="fill-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
116
|
+
className="fill-t_text_primary text-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
117
117
|
onClick={() => closeSnackbar(snackbarKey)}
|
|
118
118
|
>
|
|
119
119
|
<CloseIcon />
|
|
@@ -225,7 +225,7 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
225
225
|
View
|
|
226
226
|
</Button>
|
|
227
227
|
<div
|
|
228
|
-
className="fill-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
228
|
+
className="fill-t_text_primary text-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
229
229
|
onClick={() => closeSnackbar(snackbarKey)}
|
|
230
230
|
>
|
|
231
231
|
<CloseIcon />
|
|
@@ -321,7 +321,7 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
321
321
|
</Button>
|
|
322
322
|
|
|
323
323
|
<div
|
|
324
|
-
className="fill-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
324
|
+
className="fill-t_text_primary text-t_text_primary w-[14px] h-[14px] cursor-pointer p-[14px] leading-[0px]"
|
|
325
325
|
onClick={() => closeSnackbar(snackbarKey)}
|
|
326
326
|
>
|
|
327
327
|
<CloseIcon />
|
|
@@ -2,6 +2,14 @@ import { GAS_LIMIT_MULTIPLIER } from "@src/constants";
|
|
|
2
2
|
import { ethers } from "ethers";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
|
|
5
|
+
// RPC nodes behind a load balancer can lag right after a tx is mined (e.g. the
|
|
6
|
+
// swap's gas estimation not seeing the allowance from a just-confirmed
|
|
7
|
+
// approval), so retry the estimation with increasing delays before giving up.
|
|
8
|
+
const GAS_ESTIMATION_RETRY_DELAYS_MS = [500, 1000, 2000];
|
|
9
|
+
|
|
10
|
+
const wait = (ms: number) =>
|
|
11
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
5
13
|
export const useEvmContractApi = () =>
|
|
6
14
|
useMemo(
|
|
7
15
|
() => ({
|
|
@@ -59,11 +67,27 @@ export const useEvmContractApi = () =>
|
|
|
59
67
|
throw new Error(decodedError?.name);
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
|
-
executeCalldata: async (
|
|
70
|
+
executeCalldata: async (
|
|
71
|
+
signer,
|
|
72
|
+
transaction,
|
|
73
|
+
{ retryGasEstimation = false } = {},
|
|
74
|
+
) => {
|
|
63
75
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
let gasEstimation;
|
|
77
|
+
for (let attempt = 0; ; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
gasEstimation = await signer.estimateGas({
|
|
80
|
+
...transaction,
|
|
81
|
+
});
|
|
82
|
+
break;
|
|
83
|
+
} catch (estimationErr) {
|
|
84
|
+
const delayMs = GAS_ESTIMATION_RETRY_DELAYS_MS[attempt];
|
|
85
|
+
if (!retryGasEstimation || delayMs === undefined) {
|
|
86
|
+
throw estimationErr;
|
|
87
|
+
}
|
|
88
|
+
await wait(delayMs);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
67
91
|
return await signer.sendTransaction({
|
|
68
92
|
...transaction,
|
|
69
93
|
gasLimit: Math.floor(
|
package/src/services/api.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
GetRoutePayload,
|
|
8
8
|
GetSolanaRoutePayload,
|
|
9
9
|
GetTokenBalancesPayload,
|
|
10
|
+
ImportedTokenData,
|
|
10
11
|
EVMRoute,
|
|
11
12
|
SolanaRoute,
|
|
12
13
|
Token,
|
|
@@ -169,3 +170,39 @@ export async function getLeaderboardChange(
|
|
|
169
170
|
export async function getBridgeTokens(): Promise<BridgeToken[]> {
|
|
170
171
|
return _sendRequest<BridgeToken[]>(`/bridgeTokens`);
|
|
171
172
|
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* GET a single token balance (in wei) resolved server-side through private
|
|
176
|
+
* RPCs. Native balance when tokenAddress is the zero address.
|
|
177
|
+
* @param payload required
|
|
178
|
+
* @param abortSignal
|
|
179
|
+
*/
|
|
180
|
+
export async function getTokenBalance(
|
|
181
|
+
payload: { chainId: string; tokenAddress: string; walletAddress: string },
|
|
182
|
+
abortSignal?: AbortSignal,
|
|
183
|
+
): Promise<{ balance: string }> {
|
|
184
|
+
return _sendRequest<{ balance: string }>(
|
|
185
|
+
`/tokenBalance?${new URLSearchParams({
|
|
186
|
+
data: JSON.stringify(payload),
|
|
187
|
+
})}`,
|
|
188
|
+
{ signal: abortSignal },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* GET ERC20 token metadata (name, symbol, decimals) resolved server-side
|
|
194
|
+
* through private RPCs.
|
|
195
|
+
* @param payload required
|
|
196
|
+
* @param abortSignal
|
|
197
|
+
*/
|
|
198
|
+
export async function getTokenMetadata(
|
|
199
|
+
payload: { chainId: string; tokenAddress: string },
|
|
200
|
+
abortSignal?: AbortSignal,
|
|
201
|
+
): Promise<ImportedTokenData> {
|
|
202
|
+
return _sendRequest<ImportedTokenData>(
|
|
203
|
+
`/tokenMetadata?${new URLSearchParams({
|
|
204
|
+
data: JSON.stringify(payload),
|
|
205
|
+
})}`,
|
|
206
|
+
{ signal: abortSignal },
|
|
207
|
+
);
|
|
208
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export * from "./numbers";
|
|
|
3
3
|
export * from "./parseWeb3Error";
|
|
4
4
|
export * from "./strings";
|
|
5
5
|
|
|
6
|
-
import { Chain } from "@src/models";
|
|
6
|
+
import { BridgeTokensDictionary, Chain, Ecosystem } from "@src/models";
|
|
7
7
|
import { format } from "date-fns";
|
|
8
8
|
import { ethers } from "ethers";
|
|
9
9
|
|
|
@@ -77,6 +77,53 @@ export const resolveTokenAddress = (
|
|
|
77
77
|
?.address?.toLowerCase();
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Checks whether a transfer from `srcChain` can reach `dstChainId`,
|
|
82
|
+
* based on the chain-level support lists.
|
|
83
|
+
*/
|
|
84
|
+
export const isChainConnectionSupported = (
|
|
85
|
+
srcChain: Chain | undefined,
|
|
86
|
+
dstChainId: string | undefined,
|
|
87
|
+
bridgeUI: boolean,
|
|
88
|
+
): boolean => {
|
|
89
|
+
if (!srcChain || !dstChainId) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (srcChain.chainId === dstChainId) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const supportedDst = bridgeUI
|
|
96
|
+
? srcChain.supportedDstForBridge
|
|
97
|
+
: srcChain.supportedDstForSwap;
|
|
98
|
+
return !!supportedDst?.includes(dstChainId);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Chain-level support (supportedDstForBridge) is not enough for bridging —
|
|
102
|
+
// the specific token must have a route to the destination chain in the
|
|
103
|
+
// bridge tokens dictionary.
|
|
104
|
+
export const isBridgeTokenConnectionSupported = ({
|
|
105
|
+
token,
|
|
106
|
+
tokenChain,
|
|
107
|
+
dstChainId,
|
|
108
|
+
bridgeTokensDictionary,
|
|
109
|
+
}: {
|
|
110
|
+
token: { address: string };
|
|
111
|
+
tokenChain: Chain | undefined;
|
|
112
|
+
dstChainId: string | undefined;
|
|
113
|
+
bridgeTokensDictionary: BridgeTokensDictionary | null;
|
|
114
|
+
}): boolean => {
|
|
115
|
+
if (!tokenChain || !dstChainId || !bridgeTokensDictionary) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
const tokenAddress =
|
|
119
|
+
tokenChain.ecosystem === Ecosystem.SOLANA
|
|
120
|
+
? token.address
|
|
121
|
+
: token.address.toLowerCase();
|
|
122
|
+
return !!bridgeTokensDictionary[tokenChain.chainId]?.[tokenAddress]?.[
|
|
123
|
+
dstChainId
|
|
124
|
+
];
|
|
125
|
+
};
|
|
126
|
+
|
|
80
127
|
export const sortChains = (chains: Chain[], disabledChainIds: string[]) =>
|
|
81
128
|
[...chains]
|
|
82
129
|
|
package/src/utils/tokens.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { Chain, Token } from "@src/models";
|
|
2
|
-
import { isServer } from "@src/utils";
|
|
2
|
+
import { isETHAddressValid, isServer } from "@src/utils";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Retrieves custom tokens from localStorage and adds them to the provided chains
|
|
6
6
|
* @param chains - The original chains to which custom tokens should be added
|
|
7
7
|
* @returns A new array of chains with custom tokens added
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Lists tokens imported by the user (kept in localStorage) as chainId+address
|
|
11
|
+
* pairs, EVM only — the shape the balances endpoint accepts.
|
|
12
|
+
*/
|
|
13
|
+
export const getCustomTokenRefs = (): {
|
|
14
|
+
chainId: string;
|
|
15
|
+
tokenAddress: string;
|
|
16
|
+
}[] => {
|
|
17
|
+
const customTokensObj = !isServer
|
|
18
|
+
? JSON.parse(localStorage.getItem("custom-tokens") || "{}")
|
|
19
|
+
: {};
|
|
20
|
+
|
|
21
|
+
return Object.entries(customTokensObj).flatMap(([chainId, tokensObj]) =>
|
|
22
|
+
Object.keys(tokensObj as Record<string, Token>)
|
|
23
|
+
.filter((tokenAddress) => isETHAddressValid(tokenAddress))
|
|
24
|
+
.map((tokenAddress) => ({ chainId, tokenAddress })),
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
9
28
|
export const addCustomTokensToChains = (chains: Chain[]): Chain[] => {
|
|
10
29
|
const customTokensObj = !isServer
|
|
11
30
|
? JSON.parse(localStorage.getItem("custom-tokens") || "{}")
|
package/tailwind.config.js
CHANGED
|
@@ -96,10 +96,13 @@ export default {
|
|
|
96
96
|
x_modal: "var(--modal-width, 340px)",
|
|
97
97
|
},
|
|
98
98
|
width: {
|
|
99
|
+
// No fallback on purpose: unless the integrator sets --modal-width,
|
|
100
|
+
// the width resolves to `auto` so the widget can shrink on mobile.
|
|
101
|
+
// The default 620px cap comes from max-width below.
|
|
99
102
|
x_modal: "var(--modal-width)",
|
|
100
103
|
},
|
|
101
104
|
maxWidth: {
|
|
102
|
-
x_modal: "var(--modal-width,
|
|
105
|
+
x_modal: "var(--modal-width, 620px)",
|
|
103
106
|
},
|
|
104
107
|
animation: {
|
|
105
108
|
"spin-slow": "spin 3s linear infinite",
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { DotGreenIcon } from "@src/assets/icons";
|
|
2
|
-
import { SwapPanelType } from "@src/components/Swap/SwapView";
|
|
3
|
-
import { useSwapContext } from "@src/context";
|
|
4
|
-
import useNetworks from "@src/hooks/networkManagement/useNetworks";
|
|
5
|
-
import { Chain } from "@src/models";
|
|
6
|
-
import { useMemo } from "react";
|
|
7
|
-
|
|
8
|
-
type Props = {
|
|
9
|
-
chain: Chain;
|
|
10
|
-
type: SwapPanelType;
|
|
11
|
-
disabled: boolean;
|
|
12
|
-
onClick: (chainId: string) => void;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const ChainItem = ({ chain, type, onClick, disabled }: Props) => {
|
|
16
|
-
const {
|
|
17
|
-
networks: { evm },
|
|
18
|
-
} = useNetworks();
|
|
19
|
-
|
|
20
|
-
const { srcChain, dstChain, bridgeUI } = useSwapContext();
|
|
21
|
-
|
|
22
|
-
const currentChainId = useMemo(
|
|
23
|
-
() => (type === "source" ? srcChain?.chainId : dstChain?.chainId),
|
|
24
|
-
[dstChain, srcChain, type],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const connected = useMemo(
|
|
28
|
-
() => evm.signer && evm.chainId === chain.chainId,
|
|
29
|
-
[chain.chainId, evm.chainId, evm.signer],
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const sameChainId = useMemo(() => {
|
|
33
|
-
if (!bridgeUI) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return type === "destination" ? srcChain?.chainId : dstChain?.chainId;
|
|
38
|
-
}, [bridgeUI, dstChain?.chainId, srcChain?.chainId, type]);
|
|
39
|
-
|
|
40
|
-
const getTooltipText = () => {
|
|
41
|
-
if (chain.chainId === sameChainId) {
|
|
42
|
-
return "Transfers between the same chains are not available";
|
|
43
|
-
}
|
|
44
|
-
if (disabled) {
|
|
45
|
-
return bridgeUI
|
|
46
|
-
? "Token is not available on this chain"
|
|
47
|
-
: `Swap from ${
|
|
48
|
-
srcChain?.displayName || "the source chain"
|
|
49
|
-
} to this chain is currently not supported.`;
|
|
50
|
-
}
|
|
51
|
-
return "";
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const tooltipText = getTooltipText();
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className="group relative">
|
|
58
|
-
<div
|
|
59
|
-
onClick={() => {
|
|
60
|
-
if (chain.chainId === sameChainId || disabled) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
onClick(chain.chainId);
|
|
64
|
-
}}
|
|
65
|
-
className={`flex justify-between items-center gap-2 mt-2 mb-2 cursor-pointer p-4 border border-solid border-white border-opacity-0 hover:selected-chain-item ${
|
|
66
|
-
currentChainId === chain.chainId ? "selected-chain-item" : ""
|
|
67
|
-
} ${
|
|
68
|
-
chain.chainId === sameChainId || disabled
|
|
69
|
-
? "disabled-chain-item cursor-default"
|
|
70
|
-
: ""
|
|
71
|
-
}`}
|
|
72
|
-
>
|
|
73
|
-
<div className="flex gap-3 items-center w-full">
|
|
74
|
-
<img
|
|
75
|
-
src={chain.image}
|
|
76
|
-
alt={chain.name}
|
|
77
|
-
className="w-[34px] h-[34px]"
|
|
78
|
-
/>
|
|
79
|
-
<p>{chain.displayName}</p>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{tooltipText && (
|
|
83
|
-
<div className="w-full text-center opacity-0 group-hover:opacity-100 text-xs transition-opacity duration-200">
|
|
84
|
-
{tooltipText}
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
|
|
88
|
-
{connected && (
|
|
89
|
-
<div className="flex items-baseline gap-1">
|
|
90
|
-
<DotGreenIcon />
|
|
91
|
-
<div className="text-t_text_green">connected</div>
|
|
92
|
-
</div>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
};
|