@reservoir0x/relay-kit-ui
Version:
Relay is the Fastest and Cheapest Way to Bridge and Transact Across Chains.
754 lines • 36.9 kB
JavaScript
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useCurrencyBalance, useENSResolver, useRelayClient, useDebounceState, useWalletAddress, useDisconnected, usePreviousValueChange, useIsWalletCompatible, useFallbackState, useGasTopUpRequired } from '../../hooks/index.js';
import { formatUnits, parseUnits } from 'viem';
import { useAccount, useWalletClient } from 'wagmi';
import { useCapabilities } from 'wagmi/experimental';
import { useQueryClient } from '@tanstack/react-query';
import { calculatePriceTimeEstimate, calculateRelayerFeeProportionUsd, calculateUsdValue, extractQuoteId, getCurrentStep, getSwapEventData, isHighRelayerServiceFeeUsd, parseFees } from '../../utils/quote.js';
import { useQuote, useTokenPrice } from '@reservoir0x/relay-kit-hooks';
import { EventNames } from '../../constants/events.js';
import { ProviderOptionsContext } from '../../providers/RelayKitProvider.js';
import { addressWithFallback, isValidAddress, findSupportedWallet } from '../../utils/address.js';
import { adaptViemWallet, getDeadAddress } from '@reservoir0x/relay-sdk';
import { errorToJSON } from '../../utils/errors.js';
import { useSwapButtonCta } from '../../hooks/widget/useSwapButtonCta.js';
import { sha256 } from '../../utils/hashing.js';
import { get15MinuteInterval } from '../../utils/time.js';
// shared query options for useTokenPrice
const tokenPriceQueryOptions = {
staleTime: 60 * 1000, // 1 minute
refetchInterval: 30 * 1000, // 30 seconds
refetchOnWindowFocus: false
};
const SwapWidgetRenderer = ({ transactionModalOpen, setTransactionModalOpen, depositAddressModalOpen, fromToken: _fromToken, setFromToken: _setFromToken, toToken: _toToken, setToToken: _setToToken, defaultToAddress, defaultAmount, defaultTradeType, slippageTolerance, context, wallet, multiWalletSupportEnabled = false, linkedWallets, supportedWalletVMs, children, onAnalyticEvent, onSwapError }) => {
const [fromToken, setFromToken] = useFallbackState(_setFromToken ? _fromToken : undefined, _setFromToken
? [
_fromToken,
_setFromToken
]
: undefined);
const [toToken, setToToken] = useFallbackState(_setToToken ? _toToken : undefined, _setToToken
? [_toToken, _setToToken]
: undefined);
const providerOptionsContext = useContext(ProviderOptionsContext);
const connectorKeyOverrides = providerOptionsContext.vmConnectorKeyOverrides;
const relayClient = useRelayClient();
const { connector } = useAccount();
const walletClient = useWalletClient();
const [customToAddress, setCustomToAddress] = useState(defaultToAddress);
const [useExternalLiquidity, setUseExternalLiquidity] = useState(false);
const address = useWalletAddress(wallet, linkedWallets);
const [tradeType, setTradeType] = useState(defaultTradeType ?? 'EXACT_INPUT');
const queryClient = useQueryClient();
const [steps, setSteps] = useState(null);
const [quoteInProgress, setQuoteInProgress] = useState(null);
const [waitingForSteps, setWaitingForSteps] = useState(false);
const [details, setDetails] = useState(null);
const [gasTopUpEnabled, setGasTopUpEnabled] = useState(false);
const [abortController, setAbortController] = useState(null);
const { value: amountInputValue, debouncedValue: debouncedInputAmountValue, setValue: setAmountInputValue, debouncedControls: debouncedAmountInputControls } = useDebounceState(!defaultTradeType || defaultTradeType === 'EXACT_INPUT'
? (defaultAmount ?? '')
: '', 500);
const { value: amountOutputValue, debouncedValue: debouncedOutputAmountValue, setValue: setAmountOutputValue, debouncedControls: debouncedAmountOutputControls } = useDebounceState(defaultTradeType === 'EXPECTED_OUTPUT' ? (defaultAmount ?? '') : '', 500);
const [swapError, setSwapError] = useState(null);
const tokenPairIsCanonical = fromToken?.chainId !== undefined &&
toToken?.chainId !== undefined &&
fromToken.symbol === toToken.symbol;
const toChain = relayClient?.chains.find((chain) => chain.id === toToken?.chainId);
const fromChain = relayClient?.chains?.find((chain) => chain.id === fromToken?.chainId);
const fromChainWalletVMSupported = !fromChain?.vmType ||
supportedWalletVMs.includes(fromChain?.vmType) ||
fromChain?.id === 1337;
const toChainWalletVMSupported = !toChain?.vmType || supportedWalletVMs.includes(toChain?.vmType);
const defaultRecipient = useMemo(() => {
const _linkedWallet = linkedWallets?.find((linkedWallet) => address ===
(linkedWallet.vmType === 'evm'
? linkedWallet.address.toLowerCase()
: linkedWallet.address));
const _isValidToAddress = isValidAddress(toChain?.vmType, customToAddress ?? '', toChain?.id, !customToAddress && _linkedWallet?.address === address
? _linkedWallet?.connector
: undefined, connectorKeyOverrides);
if (multiWalletSupportEnabled &&
toChain &&
linkedWallets &&
!_isValidToAddress) {
const supportedAddress = findSupportedWallet(toChain, customToAddress, linkedWallets, connectorKeyOverrides);
return supportedAddress;
}
}, [
multiWalletSupportEnabled,
toChain,
customToAddress,
address,
linkedWallets,
setCustomToAddress
]);
const recipient = customToAddress ?? defaultRecipient ?? address;
const { value: fromBalance, queryKey: fromBalanceQueryKey, isLoading: isLoadingFromBalance, isError: fromBalanceErrorFetching, isDuneBalance: fromBalanceIsDune, hasPendingBalance: fromBalancePending } = useCurrencyBalance({
chain: fromChain,
address: address,
currency: fromToken?.address ? fromToken.address : undefined,
enabled: fromToken !== undefined,
refreshInterval: undefined,
wallet
});
const { value: toBalance, queryKey: toBalanceQueryKey, isLoading: isLoadingToBalance, isDuneBalance: toBalanceIsDune, hasPendingBalance: toBalancePending } = useCurrencyBalance({
chain: toChain,
address: recipient,
currency: toToken?.address ? toToken.address : undefined,
enabled: toToken !== undefined,
refreshInterval: undefined,
wallet
});
const invalidateBalanceQueries = useCallback(() => {
const invalidatePeriodically = (invalidateFn) => {
let maxRefreshes = 4;
let refreshCount = 0;
const timer = setInterval(() => {
if (maxRefreshes === refreshCount) {
clearInterval(timer);
return;
}
refreshCount++;
invalidateFn();
}, 3000);
};
queryClient.invalidateQueries({ queryKey: ['useDuneBalances'] });
// Dune balances are sometimes stale, because of this we need to aggressively fetch them
// for a predetermined period to make sure we get back a fresh response
if (fromBalanceIsDune) {
invalidatePeriodically(() => {
queryClient.invalidateQueries({ queryKey: fromBalanceQueryKey });
});
}
else {
queryClient.invalidateQueries({ queryKey: fromBalanceQueryKey });
}
if (toBalanceIsDune) {
invalidatePeriodically(() => {
queryClient.invalidateQueries({ queryKey: toBalanceQueryKey });
});
}
else {
queryClient.invalidateQueries({ queryKey: toBalanceQueryKey });
}
}, [
queryClient,
fromBalanceQueryKey,
toBalanceQueryKey,
toBalanceIsDune,
fromBalanceIsDune,
address
]);
const { data: capabilities } = useCapabilities({
query: {
enabled: connector &&
(connector.id === 'coinbaseWalletSDK' || connector.id === 'coinbase')
}
});
const hasAuxiliaryFundsSupport = Boolean(fromToken?.chainId
? capabilities?.[fromToken?.chainId]?.auxiliaryFunds?.supported
: false);
const isSvmSwap = fromChain?.vmType === 'svm' || toChain?.vmType === 'svm';
const isBvmSwap = fromChain?.vmType === 'bvm' || toChain?.vmType === 'bvm';
const linkedWallet = linkedWallets?.find((linkedWallet) => address ===
(linkedWallet.vmType === 'evm'
? linkedWallet.address.toLowerCase()
: linkedWallet.address) || linkedWallet.address === address);
const isRecipientLinked = (recipient
? linkedWallets?.find((wallet) => wallet.address === recipient)
: undefined) !== undefined;
const isValidFromAddress = isValidAddress(fromChain?.vmType, address ?? '', fromChain?.id, linkedWallet?.connector, connectorKeyOverrides);
const fromAddressWithFallback = addressWithFallback(fromChain?.vmType, address, fromChain?.id, linkedWallet?.connector, connectorKeyOverrides);
const isValidToAddress = isValidAddress(toChain?.vmType, recipient ?? '', toChain?.id);
const toAddressWithFallback = addressWithFallback(toChain?.vmType, recipient, toChain?.id);
const externalLiquiditySupport = useQuote(relayClient ? relayClient : undefined, wallet, fromToken && toToken
? {
user: getDeadAddress(fromChain?.vmType, fromChain?.id),
originChainId: fromToken.chainId,
destinationChainId: toToken.chainId,
originCurrency: fromToken.address,
destinationCurrency: toToken.address,
recipient: getDeadAddress(toChain?.vmType, toChain?.id),
tradeType,
appFees: providerOptionsContext.appFees,
amount: '10000000000000000000000', //Hardcode an extremely high number
referrer: relayClient?.source ?? undefined,
useExternalLiquidity: true
}
: undefined, undefined, undefined, {
refetchOnWindowFocus: false,
enabled: fromToken !== undefined &&
toToken !== undefined &&
fromChain &&
toChain &&
(fromChain.id === toChain.baseChainId ||
toChain.id === fromChain.baseChainId)
});
const supportsExternalLiquidity = tokenPairIsCanonical &&
externalLiquiditySupport.status === 'success' &&
fromChainWalletVMSupported
? true
: false;
const { displayName: toDisplayName } = useENSResolver(recipient, {
enabled: toChain?.vmType === 'evm' && isValidToAddress
});
const [currentSlippageTolerance, setCurrentSlippageTolerance] = useState(slippageTolerance);
useEffect(() => {
setCurrentSlippageTolerance(slippageTolerance);
}, [slippageTolerance]);
const { required: gasTopUpRequired, amount: _gasTopUpAmount, amountUsd: _gasTopUpAmountUsd } = useGasTopUpRequired(toChain, fromChain, toToken, recipient);
// Retrieve the price of the `from` token
const { data: fromTokenPriceData, isLoading: isLoadingFromTokenPrice } = useTokenPrice(relayClient?.baseApiUrl, {
address: fromToken?.address ?? '',
chainId: fromToken?.chainId ?? 0,
referrer: relayClient?.source
}, {
enabled: !!(fromToken?.address && fromToken.chainId),
...tokenPriceQueryOptions
});
// Retrieve the price of the `to` token
const { data: toTokenPriceData, isLoading: isLoadingToTokenPrice } = useTokenPrice(relayClient?.baseApiUrl, {
address: toToken?.address ?? '',
chainId: toToken?.chainId ?? 0,
referrer: relayClient?.source
}, {
enabled: !!(toToken?.address && toToken.chainId),
...tokenPriceQueryOptions
});
const originChainSupportsProtocolv2 = fromChain?.protocol?.v2?.depository !== undefined;
const quoteProtocol = useMemo(() => {
//Enabled only on certain chains
if (fromChain?.id && originChainSupportsProtocolv2) {
if (!fromToken && !fromTokenPriceData) {
return undefined;
}
const relevantPrice = fromTokenPriceData?.price && !isLoadingFromTokenPrice
? fromTokenPriceData.price
: undefined;
const amount = tradeType === 'EXACT_INPUT'
? debouncedInputAmountValue
: debouncedOutputAmountValue;
if (!amount) {
return undefined;
}
const usdAmount = relevantPrice
? calculateUsdValue(relevantPrice, amount)
: undefined;
return usdAmount !== undefined && usdAmount <= 100
? 'preferV2'
: undefined;
}
else {
return undefined;
}
}, [
fromTokenPriceData,
isLoadingFromTokenPrice,
debouncedInputAmountValue,
tradeType,
originChainSupportsProtocolv2
]);
const loadingProtocolVersion = fromChain?.id && originChainSupportsProtocolv2 && isLoadingFromTokenPrice;
const quoteParameters = fromToken && toToken
? {
user: fromAddressWithFallback,
originChainId: fromToken.chainId,
destinationChainId: toToken.chainId,
originCurrency: fromToken.address,
destinationCurrency: toToken.address,
recipient: toAddressWithFallback,
tradeType,
appFees: providerOptionsContext.appFees,
amount: tradeType === 'EXACT_INPUT'
? parseUnits(debouncedInputAmountValue, fromToken.decimals).toString()
: parseUnits(debouncedOutputAmountValue, toToken.decimals).toString(),
referrer: relayClient?.source ?? undefined,
useExternalLiquidity,
useDepositAddress: !fromChainWalletVMSupported || fromToken?.chainId === 1337,
refundTo: fromToken?.chainId === 1337 ? address : undefined,
slippageTolerance: slippageTolerance,
topupGas: gasTopUpEnabled && gasTopUpRequired,
protocolVersion: quoteProtocol
}
: undefined;
const onQuoteRequested = (options, config) => {
const interval = get15MinuteInterval();
const quoteRequestId = sha256({ ...options, interval });
onAnalyticEvent?.(EventNames.QUOTE_REQUESTED, {
parameters: options,
wallet_connector: linkedWallet?.connector,
chain_id_in: options?.originChainId,
chain_id_out: options?.destinationChainId,
http_config: config,
quote_request_id: quoteRequestId
});
};
const onQuoteReceived = ({ details, steps }, options) => {
const interval = get15MinuteInterval();
const quoteRequestId = sha256({ ...options, interval });
onAnalyticEvent?.(EventNames.QUOTE_RECEIVED, {
parameters: options,
wallet_connector: linkedWallet?.connector,
amount_in: details?.currencyIn?.amountFormatted,
amount_in_raw: details?.currencyIn?.amount,
currency_in: details?.currencyIn?.currency?.symbol,
chain_id_in: details?.currencyIn?.currency?.chainId,
amount_out: details?.currencyOut?.amountFormatted,
amount_out_raw: details?.currencyOut?.amount,
currency_out: details?.currencyOut?.currency?.symbol,
chain_id_out: details?.currencyOut?.currency?.chainId,
slippage_tolerance_destination_percentage: details?.slippageTolerance?.destination?.percent,
slippage_tolerance_origin_percentage: details?.slippageTolerance?.origin?.percent,
steps,
quote_request_id: quoteRequestId,
quote_id: steps ? extractQuoteId(steps) : undefined
});
};
const quoteFetchingEnabled = Boolean(relayClient &&
((tradeType === 'EXACT_INPUT' &&
debouncedInputAmountValue &&
debouncedInputAmountValue.length > 0 &&
Number(debouncedInputAmountValue) !== 0) ||
(tradeType === 'EXPECTED_OUTPUT' &&
debouncedOutputAmountValue &&
debouncedOutputAmountValue.length > 0 &&
Number(debouncedOutputAmountValue) !== 0)) &&
fromToken !== undefined &&
toToken !== undefined &&
!transactionModalOpen &&
!depositAddressModalOpen &&
!loadingProtocolVersion);
const { data: _quoteData, error: quoteError, isFetching: isFetchingQuote, executeQuote: executeSwap, queryKey: quoteQueryKey } = useQuote(relayClient ? relayClient : undefined, wallet, quoteParameters, onQuoteRequested, onQuoteReceived, {
refetchOnWindowFocus: false,
enabled: quoteFetchingEnabled,
refetchInterval: !transactionModalOpen &&
!depositAddressModalOpen &&
debouncedInputAmountValue === amountInputValue &&
debouncedOutputAmountValue === amountOutputValue
? 12000
: undefined
}, (e) => {
const errorMessage = errorToJSON(e?.response?.data?.message ? new Error(e?.response?.data?.message) : e);
const interval = get15MinuteInterval();
const quoteRequestId = sha256({ ...quoteParameters, interval });
onAnalyticEvent?.(EventNames.QUOTE_ERROR, {
wallet_connector: linkedWallet?.connector,
error_message: errorMessage,
parameters: quoteParameters,
quote_request_id: quoteRequestId,
status_code: e.response.status ?? e.status ?? ''
});
});
const invalidateQuoteQuery = useCallback(() => {
queryClient.invalidateQueries({ queryKey: quoteQueryKey });
}, [queryClient, quoteQueryKey]);
let error = _quoteData || (isFetchingQuote && quoteFetchingEnabled) ? null : quoteError;
let quote = error ? undefined : _quoteData;
const gasTopUpAmount = quote?.details?.currencyGasTopup?.amount
? BigInt(quote?.details?.currencyGasTopup?.amount)
: _gasTopUpAmount;
const gasTopUpAmountUsd = quote?.details?.currencyGasTopup?.amountUsd ?? _gasTopUpAmountUsd;
useDisconnected(address, () => {
setCustomToAddress(undefined);
});
useEffect(() => {
if (tradeType === 'EXACT_INPUT') {
const amountOut = quote?.details?.currencyOut?.amount ?? '';
setAmountOutputValue(amountOut !== ''
? formatUnits(BigInt(amountOut), Number(quote?.details?.currencyOut?.currency?.decimals ?? 18))
: '');
}
else if (tradeType === 'EXPECTED_OUTPUT') {
const amountIn = quote?.details?.currencyIn?.amount ?? '';
setAmountInputValue(amountIn !== ''
? formatUnits(BigInt(amountIn), Number(quote?.details?.currencyIn?.currency?.decimals ?? 18))
: '');
}
debouncedAmountInputControls.flush();
debouncedAmountOutputControls.flush();
}, [quote, tradeType]);
useEffect(() => {
if (useExternalLiquidity &&
!externalLiquiditySupport.isFetching &&
!supportsExternalLiquidity) {
setUseExternalLiquidity(false);
}
}, [
supportsExternalLiquidity,
useExternalLiquidity,
externalLiquiditySupport.isFetching
]);
const feeBreakdown = useMemo(() => {
const chains = relayClient?.chains;
const fromChain = chains?.find((chain) => chain.id === fromToken?.chainId);
const toChain = chains?.find((chain) => chain.id === toToken?.chainId);
return fromToken && toToken && fromChain && toChain && quote
? parseFees(toChain, fromChain, quote)
: null;
}, [quote, fromToken, toToken, relayClient]);
const totalAmount = BigInt(quote?.details?.currencyIn?.amount ?? 0n);
const hasInsufficientBalance = Boolean(!fromBalanceErrorFetching &&
totalAmount &&
address &&
(fromBalance ?? 0n) < totalAmount &&
!hasAuxiliaryFundsSupport &&
fromChainWalletVMSupported);
const fetchQuoteErrorMessage = error
? error?.message
? error?.message
: 'Unknown Error'
: null;
const fetchQuoteDataErrorMessage = error
? error?.response?.data?.message
? error?.response?.data.message
: 'Unknown Error'
: null;
const isInsufficientLiquidityError = Boolean(fetchQuoteErrorMessage?.includes('No quotes available'));
const isCapacityExceededError = fetchQuoteDataErrorMessage?.includes('Amount is higher than the available liquidity') || fetchQuoteDataErrorMessage?.includes('Insufficient relayer liquidity');
const isCouldNotExecuteError = fetchQuoteDataErrorMessage?.includes('Could not execute');
const highRelayerServiceFee = isHighRelayerServiceFeeUsd(quote);
const relayerFeeProportion = calculateRelayerFeeProportionUsd(quote);
const timeEstimate = calculatePriceTimeEstimate(quote?.details);
const canonicalTimeEstimate = calculatePriceTimeEstimate(externalLiquiditySupport.data?.details);
const recipientWalletSupportsChain = useIsWalletCompatible(toChain?.id, recipient, linkedWallets, onAnalyticEvent);
const isFromNative = fromToken?.address === fromChain?.currency?.address;
const isSameCurrencySameRecipientSwap = fromToken?.address === toToken?.address &&
fromToken?.chainId === toToken?.chainId &&
address === recipient;
const ctaCopy = useSwapButtonCta({
fromToken,
toToken,
multiWalletSupportEnabled,
isValidFromAddress,
fromChainWalletVMSupported,
isValidToAddress,
toChainWalletVMSupported,
fromChain,
toChain,
isSameCurrencySameRecipientSwap,
debouncedInputAmountValue,
debouncedOutputAmountValue,
hasInsufficientBalance,
isInsufficientLiquidityError,
quote,
operation: quote?.details?.operation
});
usePreviousValueChange(isCapacityExceededError && supportsExternalLiquidity, !isFetchingQuote && !externalLiquiditySupport.isFetching, (capacityExceeded) => {
if (capacityExceeded) {
onAnalyticEvent?.(EventNames.CTA_MAX_CAPACITY_PROMPTED, {
inputAmount: debouncedInputAmountValue,
outputAmount: debouncedOutputAmountValue
});
}
});
const swap = useCallback(async () => {
let submittedEvents = [];
const swapErrorHandler = (error, currentSteps) => {
const errorMessage = errorToJSON(error?.response?.data?.message
? new Error(error?.response?.data?.message)
: error);
if (error &&
((typeof error.message === 'string' &&
error.message.includes('rejected')) ||
(typeof error === 'string' && error.includes('rejected')) ||
(typeof error === 'string' && error.includes('Approval Denied')) ||
(typeof error === 'string' && error.includes('denied transaction')) ||
(typeof error.message === 'string' &&
error.message.includes('Approval Denied')) ||
(typeof error.message === 'string' &&
error.message.includes('Plugin Closed')) ||
(typeof error.message === 'string' &&
error.message.includes('denied transaction')) ||
(typeof error.message === 'string' &&
error.message.includes('Failed to initialize request') &&
fromChain?.id === 2741)) // Abstract @TODO: remove once privy improves handling rejected transactions
) {
// Close the transaction modal if the user rejects the tx
setTransactionModalOpen(false);
onAnalyticEvent?.(EventNames.USER_REJECTED_WALLET, {
error_message: errorMessage
});
return;
}
const { step, stepItem } = getCurrentStep(currentSteps);
const swapEventData = {
...getSwapEventData(quote?.details, currentSteps ?? null, linkedWallet?.connector, quoteParameters),
error_message: errorMessage
};
const isApproval = step?.id === 'approve';
const errorEvent = isApproval
? EventNames.APPROVAL_ERROR
: EventNames.DEPOSIT_ERROR;
//Filter out receipt/deposit transaction errors, those are approval/deposit errors
const isTransactionConfirmationError = (error &&
typeof error.message === 'string' &&
error.message.includes('TransactionConfirmationError')) ||
(error.name && error.name.includes('TransactionConfirmationError'));
if (stepItem?.receipt &&
stepItem.check &&
!isTransactionConfirmationError &&
(typeof stepItem.receipt === 'object' && 'status' in stepItem.receipt
? stepItem.receipt.status !== 'reverted'
: true) &&
(!stepItem.checkStatus || stepItem.checkStatus !== 'unknown')) {
//In some cases there's a race condition where an error is thrown before the steps get a chance to call
//the callback which triggers the success event. This is a workaround to ensure the success event is triggered when
//we have a receipt and require a fill check if we haven't already send the success event.
const successEvent = isApproval
? EventNames.APPROVAL_SUCCESS
: EventNames.DEPOSIT_SUCCESS;
if (!submittedEvents.includes(successEvent)) {
onAnalyticEvent?.(successEvent, swapEventData);
submittedEvents.push(successEvent);
//To preserve the order of events we need to delay sending the fill error event but mark that we did send it to avoid duplicates
setTimeout(() => {
onAnalyticEvent?.(EventNames.FILL_ERROR, swapEventData);
}, 20);
}
else {
onAnalyticEvent?.(EventNames.FILL_ERROR, swapEventData);
}
}
else if (!stepItem?.receipt ||
(typeof stepItem.receipt === 'object' &&
'status' in stepItem.receipt &&
stepItem.receipt.status === 'reverted')) {
onAnalyticEvent?.(errorEvent, swapEventData);
}
else {
onAnalyticEvent?.(EventNames.SWAP_ERROR, swapEventData);
}
setSwapError(errorMessage);
onSwapError?.(errorMessage, { ...quote, steps: currentSteps });
};
try {
const swapEventData = getSwapEventData(quote?.details, quote?.steps ? quote?.steps : null, linkedWallet?.connector, quoteParameters);
onAnalyticEvent?.(EventNames.SWAP_CTA_CLICKED, swapEventData);
setWaitingForSteps(true);
if (!executeSwap) {
throw 'Missing a quote';
}
if (!wallet && !walletClient.data) {
throw 'Missing a wallet';
}
setSteps(quote?.steps);
setQuoteInProgress(quote);
setTransactionModalOpen(true);
const _wallet = wallet ?? adaptViemWallet(walletClient.data);
const activeWalletChainId = await _wallet?.getChainId();
const activeWalletChain = relayClient?.chains?.find((chain) => chain.id === activeWalletChainId);
let targetChainId = fromToken?.chainId;
//Special case for Hyperliquid, to sign txs on an evm chain
if (fromToken?.chainId === 1337) {
targetChainId =
activeWalletChain?.vmType !== 'evm' ? 1 : activeWalletChainId;
}
if (fromToken && targetChainId && targetChainId !== activeWalletChainId) {
onAnalyticEvent?.(EventNames.SWAP_SWITCH_NETWORK, {
activeWalletChainId,
...swapEventData
});
await _wallet?.switchChain(targetChainId);
}
let _currentSteps = undefined;
const execPromise = executeSwap(({ steps: currentSteps }) => {
setSteps(currentSteps);
_currentSteps = currentSteps;
const { step, stepItem } = getCurrentStep(currentSteps);
const swapEventData = getSwapEventData(quote?.details, currentSteps, linkedWallet?.connector, quoteParameters);
if (step && stepItem) {
//@ts-ignore
const isApproval = step.id === 'approve' || step.id === 'approval';
let submittedEvent = isApproval
? EventNames.APPROVAL_SUBMITTED
: EventNames.DEPOSIT_SUBMITTED;
const successEvent = isApproval
? EventNames.APPROVAL_SUCCESS
: EventNames.DEPOSIT_SUCCESS;
const isBatchTransaction = Boolean(Array.isArray(step.items) &&
step.items.length > 1 &&
wallet?.handleBatchTransactionStep);
if (!isApproval && isBatchTransaction) {
submittedEvent = EventNames.BATCH_TX_SUBMITTED;
}
if (!submittedEvents.includes(submittedEvent) &&
!stepItem.receipt &&
stepItem?.txHashes &&
stepItem?.txHashes?.length > 0) {
submittedEvents.push(submittedEvent);
onAnalyticEvent?.(submittedEvent, swapEventData);
}
else if ((!submittedEvents.includes(successEvent) &&
stepItem.receipt &&
!(typeof stepItem.receipt === 'object' &&
'status' in stepItem.receipt &&
stepItem.receipt.status === 'reverted')) ||
stepItem.checkStatus === 'pending') {
onAnalyticEvent?.(successEvent, swapEventData);
submittedEvents.push(successEvent);
}
if (stepItem.status === 'complete' &&
stepItem.check &&
!submittedEvents.includes(EventNames.FILL_SUCCESS)) {
//Sometimes a fill may be quicker than the tx receipt is available, so we need to handle this scenario
if (!submittedEvents.includes(EventNames.DEPOSIT_SUCCESS) &&
!isBatchTransaction) {
onAnalyticEvent?.(EventNames.DEPOSIT_SUCCESS, swapEventData);
submittedEvents.push(EventNames.DEPOSIT_SUCCESS);
//To preserve the order of events we need to delay sending the fill success event but mark that we did send it to avoid duplicates
setTimeout(() => {
onAnalyticEvent?.(EventNames.FILL_SUCCESS, swapEventData);
}, 20);
}
else {
onAnalyticEvent?.(EventNames.FILL_SUCCESS, swapEventData);
}
submittedEvents.push(EventNames.FILL_SUCCESS);
}
}
else if (currentSteps?.every((step) => step.items?.every((item) => item.status === 'complete')) &&
!submittedEvents.includes(EventNames.FILL_SUCCESS)) {
//Sometimes a fill may be quicker than the tx receipt is available, so we need to handle this scenario
if (!submittedEvents.includes(EventNames.DEPOSIT_SUCCESS) &&
!submittedEvents.includes(EventNames.BATCH_TX_SUBMITTED)) {
onAnalyticEvent?.(EventNames.DEPOSIT_SUCCESS, swapEventData);
submittedEvents.push(EventNames.DEPOSIT_SUCCESS);
//To preserve the order of events we need to delay sending the fill success event but mark that we did send it to avoid duplicates
setTimeout(() => {
onAnalyticEvent?.(EventNames.FILL_SUCCESS, swapEventData);
}, 20);
}
else {
onAnalyticEvent?.(EventNames.FILL_SUCCESS, swapEventData);
}
submittedEvents.push(EventNames.FILL_SUCCESS);
}
});
// Store the AbortController for potential cancellation immediately
if (execPromise &&
typeof execPromise === 'object' &&
'abortController' in execPromise) {
setAbortController(execPromise.abortController);
}
execPromise
?.catch((error) => {
swapErrorHandler(error, _currentSteps);
})
.finally(() => {
setWaitingForSteps(false);
setAbortController(null);
invalidateBalanceQueries();
});
}
catch (error) {
swapErrorHandler(error);
setWaitingForSteps(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
relayClient,
address,
connector,
wallet,
walletClient,
fromToken,
toToken,
customToAddress,
recipient,
debouncedInputAmountValue,
debouncedOutputAmountValue,
tradeType,
useExternalLiquidity,
waitingForSteps,
executeSwap,
setSteps,
setQuoteInProgress,
invalidateBalanceQueries,
linkedWallet,
abortController
]);
return (_jsx(_Fragment, { children: children({
quote,
steps,
setSteps,
swap,
transactionModalOpen,
feeBreakdown,
fromToken,
setFromToken,
toToken,
setToToken,
swapError,
error,
toDisplayName,
address,
recipient,
customToAddress,
setCustomToAddress,
tradeType,
setTradeType,
details,
isSameCurrencySameRecipientSwap,
debouncedInputAmountValue,
debouncedAmountInputControls,
setAmountInputValue,
amountInputValue,
amountOutputValue,
debouncedOutputAmountValue,
debouncedAmountOutputControls,
setAmountOutputValue,
toBalance,
toBalancePending,
isLoadingToBalance,
isFetchingQuote,
isLoadingFromBalance,
fromBalance,
fromBalancePending,
highRelayerServiceFee,
relayerFeeProportion,
hasInsufficientBalance,
isInsufficientLiquidityError,
isCapacityExceededError,
isCouldNotExecuteError,
ctaCopy,
isFromNative,
useExternalLiquidity,
slippageTolerance: currentSlippageTolerance,
supportsExternalLiquidity,
timeEstimate,
canonicalTimeEstimate,
fetchingExternalLiquiditySupport: externalLiquiditySupport.isFetching,
isSvmSwap,
isBvmSwap,
isValidFromAddress,
isValidToAddress,
supportedWalletVMs,
fromChainWalletVMSupported,
toChainWalletVMSupported,
isRecipientLinked,
recipientWalletSupportsChain,
gasTopUpEnabled,
setGasTopUpEnabled,
gasTopUpRequired,
gasTopUpAmount,
gasTopUpAmountUsd,
invalidateBalanceQueries,
invalidateQuoteQuery,
setUseExternalLiquidity,
setDetails,
setSwapError,
quoteInProgress,
setQuoteInProgress,
linkedWallet,
quoteParameters,
abortController,
fromTokenPriceData,
toTokenPriceData,
isLoadingFromTokenPrice,
isLoadingToTokenPrice
}) }));
};
export default SwapWidgetRenderer;
//# sourceMappingURL=SwapWidgetRenderer.js.map