@xswap-link/sdk 0.14.0 → 0.15.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.
- package/CHANGELOG.md +21 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.global.js +92 -92
- package/dist/index.js +3208 -2862
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3083 -2740
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +6 -4
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +5 -7
- package/src/assets/icons/ChainlinkMarkIcon.tsx +4 -5
- package/src/assets/icons/CheckCircleThinIcon.tsx +22 -0
- package/src/assets/icons/ChevronDownThinIcon.tsx +39 -0
- package/src/assets/icons/ChevronDownWideThinIcon.tsx +18 -0
- package/src/assets/icons/ChevronsUpDownThinIcon.tsx +21 -0
- package/src/assets/icons/ClockIcon.tsx +3 -7
- package/src/assets/icons/CloseIcon.tsx +9 -4
- package/src/assets/icons/ErrorCircleThinIcon.tsx +22 -0
- package/src/assets/icons/LinkExternalThinIcon.tsx +19 -0
- package/src/assets/icons/LoadingRingIcon.tsx +22 -0
- package/src/assets/icons/ProgressRingThinIcon.tsx +29 -0
- package/src/assets/icons/SearchThinIcon.tsx +15 -0
- package/src/assets/icons/SignThinIcon.tsx +15 -0
- package/src/assets/icons/WalletCheckThinIcon.tsx +24 -0
- package/src/assets/icons/WarningIcon.tsx +8 -5
- package/src/assets/icons/index.ts +11 -0
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +6 -8
- package/src/components/Swap/Header/index.tsx +2 -4
- package/src/components/Swap/HistoryView/ActivityCard/index.tsx +110 -115
- package/src/components/Swap/HistoryView/index.tsx +10 -7
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +4 -4
- package/src/components/Swap/SettingsView/Delivery/index.tsx +19 -17
- package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +19 -17
- package/src/components/Swap/SettingsView/Slippage/index.tsx +55 -49
- package/src/components/Swap/SwapView/ConfirmationView/TokenTiles/index.tsx +69 -0
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +13 -25
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Steps/index.tsx +65 -113
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +68 -26
- package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +51 -74
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +1 -1
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +5 -5
- package/src/components/Swap/SwapView/SwapButton/index.tsx +2 -31
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +19 -16
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +11 -4
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +185 -105
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +54 -120
- package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +10 -16
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +4 -4
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +131 -139
- package/src/components/Swap/SwapView/index.tsx +11 -7
- package/src/components/Swap/WalletPickerView/index.tsx +65 -0
- package/src/components/Swap/index.tsx +39 -8
- package/src/components/ToggleButton/index.tsx +5 -7
- package/src/components/TokenLogo/index.tsx +1 -1
- package/src/components/TxConfigForm/index.tsx +2 -2
- package/src/components/TxModal/index.tsx +14 -4
- package/src/components/global.css +18 -1
- package/src/constants/index.ts +4 -0
- package/src/context/HistoryProvider.tsx +1 -1
- package/src/context/SwapProvider.tsx +193 -81
- package/src/context/TxUIWrapper.tsx +3 -3
- package/src/hooks/useEvmContractApi.ts +28 -4
- package/src/models/payloads/GetBalancesPayload.ts +1 -0
- package/src/services/api.ts +37 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/tokens.ts +20 -1
- package/src/utils/validation.ts +9 -4
- package/tailwind.config.js +4 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/ArrowIcon.tsx +0 -13
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/SwapPanel/index.tsx +0 -72
- package/src/components/Swap/SwapView/ConfirmationView/TxResult/TokenItem/index.tsx +0 -25
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { TxConfigFormPayload } from "@src/components";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_BRIDGE_DESTINATION_CHAIN_ID,
|
|
4
|
+
DEFAULT_BRIDGE_SOURCE_CHAIN_ID,
|
|
3
5
|
DEFAULT_DESTINATION_CHAIN_ID,
|
|
4
6
|
DEFAULT_REQUEST_ABORT_MSG,
|
|
5
7
|
DEFAULT_SOURCE_CHAIN_ID,
|
|
@@ -28,10 +30,11 @@ import {
|
|
|
28
30
|
getPrices,
|
|
29
31
|
getRoute,
|
|
30
32
|
getSolanaRoute,
|
|
33
|
+
getTokenBalance,
|
|
31
34
|
} from "@src/services";
|
|
35
|
+
import { getCustomTokenRefs } from "@src/utils/tokens";
|
|
32
36
|
import {
|
|
33
37
|
deepMergeObjects,
|
|
34
|
-
getBalanceOf,
|
|
35
38
|
humanReadableToWei,
|
|
36
39
|
isServer,
|
|
37
40
|
resolveTokenAddress,
|
|
@@ -129,6 +132,10 @@ type SwapContext = {
|
|
|
129
132
|
chainId: string | undefined,
|
|
130
133
|
token: TokenOption,
|
|
131
134
|
) => void;
|
|
135
|
+
// Which side's token/network picker is open. The picker renders in place
|
|
136
|
+
// of the swap view (desktop) or as a bottom sheet (mobile).
|
|
137
|
+
activePicker: "source" | "destination" | null;
|
|
138
|
+
setActivePicker: Dispatch<SetStateAction<"source" | "destination" | null>>;
|
|
132
139
|
};
|
|
133
140
|
|
|
134
141
|
const functionNotImplemented = () => {
|
|
@@ -142,6 +149,8 @@ const init: SwapContext = {
|
|
|
142
149
|
tab: "Swap",
|
|
143
150
|
overlay: false,
|
|
144
151
|
error: "",
|
|
152
|
+
activePicker: null,
|
|
153
|
+
setActivePicker: functionNotImplemented,
|
|
145
154
|
feeToken: undefined,
|
|
146
155
|
srcChain: undefined,
|
|
147
156
|
srcChainTokensOptions: [],
|
|
@@ -224,6 +233,7 @@ export const SwapProvider = ({
|
|
|
224
233
|
// Settings
|
|
225
234
|
const [tab, setTab] = useState(init.tab);
|
|
226
235
|
const [error, setError] = useState(init.error);
|
|
236
|
+
const [activePicker, setActivePicker] = useState(init.activePicker);
|
|
227
237
|
const [expressDelivery, setExpressDelivery] = useState(init.expressDelivery);
|
|
228
238
|
const [slippage, setSlippage] = useState(init.slippage);
|
|
229
239
|
const [infiniteApproval, setInfiniteApproval] = useState(
|
|
@@ -240,6 +250,9 @@ export const SwapProvider = ({
|
|
|
240
250
|
const [tokenBalances, setTokenBalances] = useState<
|
|
241
251
|
Record<string, Record<string, string>>
|
|
242
252
|
>({});
|
|
253
|
+
// bumped when the user imports a token, so the supported tokens mapping and
|
|
254
|
+
// the balances list get rebuilt with the new custom token included
|
|
255
|
+
const [customTokensVersion, setCustomTokensVersion] = useState(0);
|
|
243
256
|
const [bridgeTokens, setBridgeTokens] = useState<BridgeToken[]>([]);
|
|
244
257
|
// Values
|
|
245
258
|
const [srcValue, setSrcValue] = useState(init.srcValue);
|
|
@@ -280,6 +293,19 @@ export const SwapProvider = ({
|
|
|
280
293
|
const routeAbortController = useRef<AbortController | null>(null);
|
|
281
294
|
const refreshSelectedTokenBalanceNonce = useRef(crypto.randomUUID());
|
|
282
295
|
const prevWalletAddresses = useRef<string | undefined>(undefined);
|
|
296
|
+
// last wallet chain id seen by the follow-the-wallet effect below
|
|
297
|
+
const prevFollowedWalletChainId = useRef<string | undefined>(undefined);
|
|
298
|
+
// True between the bridgeUI-change reset and the re-init that follows it.
|
|
299
|
+
// Effects in the reset's commit still close over the pre-reset state (with
|
|
300
|
+
// the new bridgeUI), so acting on that mix would remap the old mode's route
|
|
301
|
+
// as the new mode's — this ref lets them bail out synchronously.
|
|
302
|
+
const initPending = useRef(false);
|
|
303
|
+
// True while a bridgeUI-change re-init is pending. The web-component
|
|
304
|
+
// wrapper delivers attribute updates one by one, so at re-init time the
|
|
305
|
+
// route config may still hold the previous mode's values (there is no way
|
|
306
|
+
// to tell them apart from intentional ones) — the re-init therefore
|
|
307
|
+
// ignores route config entirely and starts from the new mode's defaults.
|
|
308
|
+
const modeSwitchReinit = useRef(false);
|
|
283
309
|
|
|
284
310
|
// =============================================================================
|
|
285
311
|
// Memoized Values
|
|
@@ -334,8 +360,9 @@ export const SwapProvider = ({
|
|
|
334
360
|
});
|
|
335
361
|
});
|
|
336
362
|
return deepMergeObjects(chainIdToTokenAddressToData, customTokens);
|
|
363
|
+
// customTokensVersion re-reads localStorage after an in-session import
|
|
337
364
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
-
}, [supportedChains]);
|
|
365
|
+
}, [supportedChains, customTokensVersion]);
|
|
339
366
|
|
|
340
367
|
// useMemo: creates a mapping from each chain ID to available token options sorted by token priority.
|
|
341
368
|
const chainToTokenOptionsMap = useMemo(() => {
|
|
@@ -679,6 +706,12 @@ export const SwapProvider = ({
|
|
|
679
706
|
!srcToken ||
|
|
680
707
|
!srcToken?.address
|
|
681
708
|
) {
|
|
709
|
+
// Nothing to read a balance for — drop the previous selection's
|
|
710
|
+
// balance (and invalidate any in-flight read) so the MAX button
|
|
711
|
+
// never acts on a stale value.
|
|
712
|
+
refreshSelectedTokenBalanceNonce.current = crypto.randomUUID();
|
|
713
|
+
setSrcTokenBalanceWei("");
|
|
714
|
+
setIsFetchingBalance(false);
|
|
682
715
|
return;
|
|
683
716
|
}
|
|
684
717
|
|
|
@@ -686,68 +719,70 @@ export const SwapProvider = ({
|
|
|
686
719
|
refreshSelectedTokenBalanceNonce.current = nonce;
|
|
687
720
|
setIsFetchingBalance(true);
|
|
688
721
|
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
|
-
},
|
|
722
|
+
try {
|
|
723
|
+
if (srcChain?.ecosystem === Ecosystem.EVM && evmAddress) {
|
|
724
|
+
const balanceFromBackend =
|
|
725
|
+
tokenBalances[srcChain?.chainId]?.[srcToken.address];
|
|
726
|
+
if (options?.forceRpc || balanceFromBackend === undefined) {
|
|
727
|
+
// single-token read resolved server-side through private RPCs
|
|
728
|
+
({ balance } = await getTokenBalance({
|
|
729
|
+
chainId: srcChain.chainId,
|
|
730
|
+
tokenAddress: srcToken.address,
|
|
731
|
+
walletAddress: evmAddress,
|
|
714
732
|
}));
|
|
733
|
+
if (options?.forceRpc && balanceFromBackend !== undefined) {
|
|
734
|
+
// keep the backend cache in sync so token pickers and future
|
|
735
|
+
// non-forced refreshes see the fresh value
|
|
736
|
+
const chainId = srcChain.chainId;
|
|
737
|
+
const tokenAddress = srcToken.address;
|
|
738
|
+
const freshBalance = balance;
|
|
739
|
+
setTokenBalances((prev) => ({
|
|
740
|
+
...prev,
|
|
741
|
+
[chainId]: {
|
|
742
|
+
...prev[chainId],
|
|
743
|
+
[tokenAddress]: freshBalance,
|
|
744
|
+
},
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
balance = balanceFromBackend;
|
|
715
749
|
}
|
|
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";
|
|
750
|
+
} else if (srcChain?.ecosystem === Ecosystem.SOLANA && solanaAddress) {
|
|
751
|
+
const solanaPublicKey = new PublicKey(solanaAddress);
|
|
752
|
+
const connection = new Connection(srcChain.publicRpcUrls[0]!);
|
|
753
|
+
if (srcToken.address === SOLANA_NATIVE_TOKEN_ADDRESS.toBase58()) {
|
|
754
|
+
balance = new BigNumberJS(
|
|
755
|
+
await connection.getBalance(solanaPublicKey),
|
|
756
|
+
).toString();
|
|
747
757
|
} else {
|
|
748
|
-
|
|
758
|
+
const tokenAccounts =
|
|
759
|
+
await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
|
|
760
|
+
programId: SOLANA_TOKEN_PROGRAM_ID,
|
|
761
|
+
});
|
|
762
|
+
const token2022Accounts =
|
|
763
|
+
await connection.getParsedTokenAccountsByOwner(solanaPublicKey, {
|
|
764
|
+
programId: SOLANA_TOKEN_2022_PROGRAM_ID,
|
|
765
|
+
});
|
|
766
|
+
const allTokenAccount = [
|
|
767
|
+
...tokenAccounts.value,
|
|
768
|
+
...token2022Accounts.value,
|
|
769
|
+
];
|
|
770
|
+
const tokenAccount = allTokenAccount.find(
|
|
771
|
+
(account) =>
|
|
772
|
+
account.account.data.parsed.info.mint === srcToken.address,
|
|
773
|
+
);
|
|
774
|
+
if (!tokenAccount) {
|
|
775
|
+
balance = "0";
|
|
776
|
+
} else {
|
|
777
|
+
balance =
|
|
778
|
+
tokenAccount.account.data.parsed.info.tokenAmount.amount;
|
|
779
|
+
}
|
|
749
780
|
}
|
|
750
781
|
}
|
|
782
|
+
} catch (_) {
|
|
783
|
+
// failed read — keep the loader on: the balance must be loaded before
|
|
784
|
+
// a swap is allowed, so we never show a stale value as loaded
|
|
785
|
+
return;
|
|
751
786
|
}
|
|
752
787
|
if (nonce !== refreshSelectedTokenBalanceNonce.current) {
|
|
753
788
|
return;
|
|
@@ -790,6 +825,10 @@ export const SwapProvider = ({
|
|
|
790
825
|
? chain
|
|
791
826
|
: { ...chain, tokens: [...chain.tokens, token] };
|
|
792
827
|
|
|
828
|
+
if (!tokenExists) {
|
|
829
|
+
setCustomTokensVersion((version) => version + 1);
|
|
830
|
+
}
|
|
831
|
+
|
|
793
832
|
setSrcChain(updated);
|
|
794
833
|
setSrcToken(token);
|
|
795
834
|
},
|
|
@@ -821,8 +860,9 @@ export const SwapProvider = ({
|
|
|
821
860
|
quoteRoute();
|
|
822
861
|
}, [quoteRoute]);
|
|
823
862
|
// calls refreshBalance to update source token balance on mount or when refreshBalance changes.
|
|
824
|
-
// when the connected wallet changed, force
|
|
825
|
-
// balances still
|
|
863
|
+
// when the connected wallet changed, force a fresh single-token read — the
|
|
864
|
+
// cached balances list still belongs to the previous wallet until
|
|
865
|
+
// getBalances resolves.
|
|
826
866
|
useEffect(() => {
|
|
827
867
|
const walletAddresses = `${evmAddress ?? ""}:${solanaAddress ?? ""}`;
|
|
828
868
|
const walletChanged =
|
|
@@ -832,6 +872,8 @@ export const SwapProvider = ({
|
|
|
832
872
|
refreshBalance(walletChanged ? { forceRpc: true } : undefined);
|
|
833
873
|
}, [refreshBalance, evmAddress, solanaAddress]);
|
|
834
874
|
// fetches user all token balances when a wallet address is available and supported chains are loaded.
|
|
875
|
+
// imported tokens from localStorage are passed along so their balances get
|
|
876
|
+
// resolved server-side too.
|
|
835
877
|
useEffect(() => {
|
|
836
878
|
let isCancelled = false;
|
|
837
879
|
if ((evmAddress || solanaAddress) && supportedChains.length > 0) {
|
|
@@ -839,6 +881,7 @@ export const SwapProvider = ({
|
|
|
839
881
|
getBalances({
|
|
840
882
|
walletAddress: evmAddress,
|
|
841
883
|
solanaWalletAddress: solanaAddress,
|
|
884
|
+
customTokens: getCustomTokenRefs(),
|
|
842
885
|
}).then((balances) => {
|
|
843
886
|
if (!isCancelled) {
|
|
844
887
|
setTokenBalances(balances);
|
|
@@ -850,7 +893,7 @@ export const SwapProvider = ({
|
|
|
850
893
|
return () => {
|
|
851
894
|
isCancelled = true; // prevent state update from stale async call
|
|
852
895
|
};
|
|
853
|
-
}, [evmAddress, solanaAddress, supportedChains.length]);
|
|
896
|
+
}, [evmAddress, solanaAddress, supportedChains.length, customTokensVersion]);
|
|
854
897
|
// sets the fee token from the source chain's tokens by selecting the native token (AddressZero).
|
|
855
898
|
useEffect(() => {
|
|
856
899
|
if (srcChain?.chainId === "mainnet-beta") {
|
|
@@ -1055,9 +1098,23 @@ export const SwapProvider = ({
|
|
|
1055
1098
|
// useEffects responsible for state updates and validations
|
|
1056
1099
|
// =============================================================================
|
|
1057
1100
|
|
|
1058
|
-
//
|
|
1101
|
+
// Follows a real in-session wallet network switch with the source chain.
|
|
1102
|
+
// Only a defined -> defined change counts: the chain id first appearing
|
|
1103
|
+
// during reconnect is ignored, since wagmi restores a persisted (possibly
|
|
1104
|
+
// stale) chain id before the connector reports the actual one. Bridge keeps
|
|
1105
|
+
// its fixed default route instead of following the wallet chain.
|
|
1059
1106
|
useEffect(() => {
|
|
1060
|
-
|
|
1107
|
+
const prevWalletChainId = prevFollowedWalletChainId.current;
|
|
1108
|
+
prevFollowedWalletChainId.current = walletChainId;
|
|
1109
|
+
|
|
1110
|
+
if (
|
|
1111
|
+
!walletChainId ||
|
|
1112
|
+
!prevWalletChainId ||
|
|
1113
|
+
prevWalletChainId === walletChainId ||
|
|
1114
|
+
!isInitialStateSet ||
|
|
1115
|
+
integrationConfig?.srcChainId ||
|
|
1116
|
+
bridgeUI
|
|
1117
|
+
) {
|
|
1061
1118
|
return;
|
|
1062
1119
|
}
|
|
1063
1120
|
const initSrcChain = supportedChains.find(
|
|
@@ -1071,18 +1128,29 @@ export const SwapProvider = ({
|
|
|
1071
1128
|
isInitialStateSet,
|
|
1072
1129
|
supportedChains,
|
|
1073
1130
|
integrationConfig?.srcChainId,
|
|
1131
|
+
bridgeUI,
|
|
1074
1132
|
]);
|
|
1075
1133
|
|
|
1076
|
-
// resets
|
|
1134
|
+
// resets the widget state when bridgeUI changes at runtime: back to the
|
|
1135
|
+
// main view (leaving Settings/History), cleared selection and amount, and
|
|
1136
|
+
// re-arms the init effect below so the new mode gets its default route
|
|
1137
|
+
// (config -> wallet chain -> mode defaults) applied again.
|
|
1077
1138
|
useEffect(() => {
|
|
1078
1139
|
if (!isInitialStateSet) {
|
|
1079
1140
|
return;
|
|
1080
1141
|
}
|
|
1081
1142
|
|
|
1143
|
+
initPending.current = true;
|
|
1144
|
+
modeSwitchReinit.current = true;
|
|
1145
|
+
setTab(init.tab);
|
|
1146
|
+
setActivePicker(null);
|
|
1147
|
+
setError(init.error);
|
|
1148
|
+
setSrcValue(init.srcValue);
|
|
1082
1149
|
setSrcToken(undefined);
|
|
1083
1150
|
setDstToken(undefined);
|
|
1084
1151
|
setSrcChain(undefined);
|
|
1085
1152
|
setDstChain(undefined);
|
|
1153
|
+
setIsInitialStateSet(false);
|
|
1086
1154
|
|
|
1087
1155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1088
1156
|
}, [bridgeUI]);
|
|
@@ -1098,21 +1166,51 @@ export const SwapProvider = ({
|
|
|
1098
1166
|
return;
|
|
1099
1167
|
}
|
|
1100
1168
|
|
|
1169
|
+
// During a bridgeUI-change re-init the route config may still hold the
|
|
1170
|
+
// previous mode's values (web-component attribute updates arrive one by
|
|
1171
|
+
// one), so it is ignored — the new mode starts from its own defaults.
|
|
1172
|
+
// Route config still seeds the very first init (deep links, integrator
|
|
1173
|
+
// presets).
|
|
1174
|
+
const ignoreRouteConfig = modeSwitchReinit.current;
|
|
1175
|
+
const cfgSrcChainId = ignoreRouteConfig
|
|
1176
|
+
? undefined
|
|
1177
|
+
: integrationConfig?.srcChainId;
|
|
1178
|
+
const cfgDstChainId = ignoreRouteConfig
|
|
1179
|
+
? undefined
|
|
1180
|
+
: integrationConfig?.dstChainId;
|
|
1181
|
+
const cfgSrcTokenAddr = ignoreRouteConfig
|
|
1182
|
+
? undefined
|
|
1183
|
+
: integrationConfig?.srcTokenAddr;
|
|
1184
|
+
const cfgDstTokenAddr = ignoreRouteConfig
|
|
1185
|
+
? undefined
|
|
1186
|
+
: integrationConfig?.dstTokenAddr;
|
|
1187
|
+
|
|
1101
1188
|
// find chains
|
|
1189
|
+
// Default routes are fixed (swap: ETH -> ETH, bridge: ETH -> Base) and do
|
|
1190
|
+
// not follow the wallet's chain — on load wagmi can still report a stale,
|
|
1191
|
+
// persisted chain id, so it can't be trusted here. Integrator config
|
|
1192
|
+
// still takes precedence, and a real in-session network switch is
|
|
1193
|
+
// followed by the effect below.
|
|
1102
1194
|
const searchedSrcChainId =
|
|
1103
|
-
srcChain ||
|
|
1104
|
-
|
|
1105
|
-
(
|
|
1106
|
-
(bridgeUI ? undefined : DEFAULT_SOURCE_CHAIN_ID);
|
|
1195
|
+
srcChain?.chainId ||
|
|
1196
|
+
cfgSrcChainId ||
|
|
1197
|
+
(bridgeUI ? DEFAULT_BRIDGE_SOURCE_CHAIN_ID : DEFAULT_SOURCE_CHAIN_ID);
|
|
1107
1198
|
|
|
1108
1199
|
const initSrcChain = supportedChains.find(
|
|
1109
1200
|
({ chainId }) => chainId === searchedSrcChainId,
|
|
1110
1201
|
);
|
|
1111
1202
|
|
|
1203
|
+
// Bridge can't route within one chain, so when the source already sits on
|
|
1204
|
+
// the default bridge destination, fall back to the reversed default route.
|
|
1205
|
+
const bridgeDefaultDstChainId =
|
|
1206
|
+
initSrcChain?.chainId === DEFAULT_BRIDGE_DESTINATION_CHAIN_ID
|
|
1207
|
+
? DEFAULT_BRIDGE_SOURCE_CHAIN_ID
|
|
1208
|
+
: DEFAULT_BRIDGE_DESTINATION_CHAIN_ID;
|
|
1209
|
+
|
|
1112
1210
|
const searchedDstChainId =
|
|
1113
|
-
dstChain ||
|
|
1114
|
-
|
|
1115
|
-
(bridgeUI ?
|
|
1211
|
+
dstChain?.chainId ||
|
|
1212
|
+
cfgDstChainId ||
|
|
1213
|
+
(bridgeUI ? bridgeDefaultDstChainId : DEFAULT_DESTINATION_CHAIN_ID);
|
|
1116
1214
|
|
|
1117
1215
|
const initDstChain = supportedChains.find(
|
|
1118
1216
|
({ chainId }) => chainId === searchedDstChainId,
|
|
@@ -1123,13 +1221,13 @@ export const SwapProvider = ({
|
|
|
1123
1221
|
let initSrcToken: Token | undefined;
|
|
1124
1222
|
|
|
1125
1223
|
const srcTokenAddress = resolveTokenAddress(
|
|
1126
|
-
|
|
1127
|
-
|
|
1224
|
+
cfgSrcTokenAddr,
|
|
1225
|
+
cfgSrcChainId,
|
|
1128
1226
|
supportedChains,
|
|
1129
1227
|
);
|
|
1130
1228
|
const dstTokenAddress = resolveTokenAddress(
|
|
1131
|
-
|
|
1132
|
-
|
|
1229
|
+
cfgDstTokenAddr,
|
|
1230
|
+
cfgDstChainId,
|
|
1133
1231
|
supportedChains,
|
|
1134
1232
|
);
|
|
1135
1233
|
|
|
@@ -1159,6 +1257,8 @@ export const SwapProvider = ({
|
|
|
1159
1257
|
setSrcToken(source.token);
|
|
1160
1258
|
setDstToken(target.token);
|
|
1161
1259
|
// Mark initial state as set
|
|
1260
|
+
modeSwitchReinit.current = false;
|
|
1261
|
+
initPending.current = false;
|
|
1162
1262
|
setIsInitialStateSet(true);
|
|
1163
1263
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1164
1264
|
}, [
|
|
@@ -1177,7 +1277,11 @@ export const SwapProvider = ({
|
|
|
1177
1277
|
if (
|
|
1178
1278
|
!bridgeTokensDictionary ||
|
|
1179
1279
|
supportedChains.length === 0 ||
|
|
1180
|
-
!isInitialStateSet
|
|
1280
|
+
!isInitialStateSet ||
|
|
1281
|
+
// A bridgeUI-change reset is in flight: this closure still holds the
|
|
1282
|
+
// previous mode's route, so mapping it against the new mode would
|
|
1283
|
+
// resurrect it. The re-init applies the new mode's defaults instead.
|
|
1284
|
+
initPending.current
|
|
1181
1285
|
) {
|
|
1182
1286
|
return;
|
|
1183
1287
|
}
|
|
@@ -1218,9 +1322,15 @@ export const SwapProvider = ({
|
|
|
1218
1322
|
bridgeTokensDictionary,
|
|
1219
1323
|
isInitialStateSet,
|
|
1220
1324
|
]);
|
|
1325
|
+
// Notify effects below also re-run whenever the integrator recreates the
|
|
1326
|
+
// callback props (inline arrows give them a new identity every render).
|
|
1327
|
+
// During a bridgeUI-change reset their closures still hold the previous
|
|
1328
|
+
// mode's route, and re-emitting it would feed it back to the integrator as
|
|
1329
|
+
// config — the initPending guard keeps them quiet until the re-init lands.
|
|
1330
|
+
|
|
1221
1331
|
// notifies integration or global callback when the destination chain changes.
|
|
1222
1332
|
useEffect(() => {
|
|
1223
|
-
if (!isInitialStateSet) {
|
|
1333
|
+
if (!isInitialStateSet || initPending.current) {
|
|
1224
1334
|
return;
|
|
1225
1335
|
}
|
|
1226
1336
|
if (integrationConfig.onDstChainChange) {
|
|
@@ -1232,7 +1342,7 @@ export const SwapProvider = ({
|
|
|
1232
1342
|
}, [integrationConfig.onDstChainChange, dstChain]);
|
|
1233
1343
|
// notifies integration or global callback when the destination token changes.
|
|
1234
1344
|
useEffect(() => {
|
|
1235
|
-
if (!isInitialStateSet) {
|
|
1345
|
+
if (!isInitialStateSet || initPending.current) {
|
|
1236
1346
|
return;
|
|
1237
1347
|
}
|
|
1238
1348
|
const token = dstToken
|
|
@@ -1248,7 +1358,7 @@ export const SwapProvider = ({
|
|
|
1248
1358
|
}, [integrationConfig.onDstTokenChange, dstToken]);
|
|
1249
1359
|
// notifies integration or global callback when the source chain changes.
|
|
1250
1360
|
useEffect(() => {
|
|
1251
|
-
if (!isInitialStateSet) {
|
|
1361
|
+
if (!isInitialStateSet || initPending.current) {
|
|
1252
1362
|
return;
|
|
1253
1363
|
}
|
|
1254
1364
|
if (integrationConfig.onSrcChainChange) {
|
|
@@ -1260,7 +1370,7 @@ export const SwapProvider = ({
|
|
|
1260
1370
|
}, [integrationConfig.onSrcChainChange, srcChain]);
|
|
1261
1371
|
// notifies integration or global callback when the source token changes.
|
|
1262
1372
|
useEffect(() => {
|
|
1263
|
-
if (!isInitialStateSet) {
|
|
1373
|
+
if (!isInitialStateSet || initPending.current) {
|
|
1264
1374
|
return;
|
|
1265
1375
|
}
|
|
1266
1376
|
const token = srcToken
|
|
@@ -1337,6 +1447,8 @@ export const SwapProvider = ({
|
|
|
1337
1447
|
onConnectWallet,
|
|
1338
1448
|
hasOnConnectWalletCallback,
|
|
1339
1449
|
addTokenToChainIfNotExists,
|
|
1450
|
+
activePicker,
|
|
1451
|
+
setActivePicker,
|
|
1340
1452
|
}}
|
|
1341
1453
|
>
|
|
1342
1454
|
{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
|
|
|
@@ -98,6 +98,32 @@ export const isChainConnectionSupported = (
|
|
|
98
98
|
return !!supportedDst?.includes(dstChainId);
|
|
99
99
|
};
|
|
100
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
|
+
|
|
101
127
|
export const sortChains = (chains: Chain[], disabledChainIds: string[]) =>
|
|
102
128
|
[...chains]
|
|
103
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") || "{}")
|