diff --git a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx index 91ff745b5..0d7dfa151 100644 --- a/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx @@ -11,7 +11,7 @@ import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN_SYMBOL } from '@/constants' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' import { AccountType, Account } from '@/interfaces' -import { formatIban, shortenAddressLong } from '@/utils/general.utils' +import { formatIban, shortenAddressLong, isTxReverted } from '@/utils/general.utils' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import DirectSuccessView from '@/components/Payment/Views/Status.payment.view' @@ -134,7 +134,7 @@ export default function WithdrawBankPage() { // Step 2: prepare and send the transaction from peanut wallet to the deposit address const receipt = await sendMoney(data.depositInstructions.toAddress as `0x${string}`, createPayload.amount) - if (receipt.status === 'reverted') { + if (isTxReverted(receipt)) { throw new Error('Transaction reverted by the network.') } diff --git a/src/app/(mobile-ui)/withdraw/crypto/page.tsx b/src/app/(mobile-ui)/withdraw/crypto/page.tsx index cfb6b8d04..de4c224c5 100644 --- a/src/app/(mobile-ui)/withdraw/crypto/page.tsx +++ b/src/app/(mobile-ui)/withdraw/crypto/page.tsx @@ -22,7 +22,7 @@ import { } from '@/services/services.types' import { NATIVE_TOKEN_ADDRESS } from '@/utils/token.utils' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, ROUTE_NOT_FOUND_ERROR } from '@/constants' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useMemo } from 'react' import { captureMessage } from '@sentry/nextjs' @@ -287,7 +287,7 @@ export default function WithdrawCryptoPage() { routeObject: xChainRoute, }, }) - return 'No route found for this token pair. You can try with a different token pair, or try again later' + return ROUTE_NOT_FOUND_ERROR } return null diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 756892e81..90aff2f48 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -13,7 +13,13 @@ import { } from '@/components/Offramp/Offramp.consts' import { ActionType, estimatePoints } from '@/components/utils/utils' import * as consts from '@/constants' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN, PINTA_WALLET_CHAIN, PINTA_WALLET_TOKEN } from '@/constants' +import { + PEANUT_WALLET_CHAIN, + PEANUT_WALLET_TOKEN, + PINTA_WALLET_CHAIN, + PINTA_WALLET_TOKEN, + ROUTE_NOT_FOUND_ERROR, +} from '@/constants' import { TRANSACTIONS } from '@/constants/query.consts' import { loadingStateContext, tokenSelectorContext } from '@/context' import { useAuth } from '@/context/authContext' @@ -406,7 +412,7 @@ export const InitialClaimLinkView = ({ } setErrorState({ showError: true, - errorMessage: 'No route found for the given token pair.', + errorMessage: ROUTE_NOT_FOUND_ERROR, }) Sentry.captureException(error) return undefined diff --git a/src/components/Payment/Views/Confirm.payment.view.tsx b/src/components/Payment/Views/Confirm.payment.view.tsx index 996bf10a5..0a754c642 100644 --- a/src/components/Payment/Views/Confirm.payment.view.tsx +++ b/src/components/Payment/Views/Confirm.payment.view.tsx @@ -21,7 +21,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useAppDispatch, usePaymentStore, useWalletStore } from '@/redux/hooks' import { paymentActions } from '@/redux/slices/payment-slice' import { chargesApi } from '@/services/charges' -import { ErrorHandler, formatAmount, areEvmAddressesEqual } from '@/utils' +import { ErrorHandler, formatAmount, areEvmAddressesEqual, isStableCoin } from '@/utils' import { useQueryClient } from '@tanstack/react-query' import { useSearchParams } from 'next/navigation' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' @@ -29,7 +29,12 @@ import { useAccount } from 'wagmi' import { PaymentInfoRow } from '../PaymentInfoRow' import { formatUnits } from 'viem' import type { Address } from 'viem' -import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import { + PEANUT_WALLET_CHAIN, + PEANUT_WALLET_TOKEN, + ROUTE_NOT_FOUND_ERROR, + PEANUT_WALLET_TOKEN_SYMBOL, +} from '@/constants' import { captureMessage } from '@sentry/nextjs' import AddressLink from '@/components/Global/AddressLink' @@ -90,9 +95,7 @@ export default function ConfirmPaymentView({ const queryClient = useQueryClient() const [isRouteExpired, setIsRouteExpired] = useState(false) - const isUsingExternalWallet = useMemo(() => { - return isAddMoneyFlow || !isPeanutWallet - }, [isPeanutWallet, isAddMoneyFlow]) + const isUsingExternalWallet = isAddMoneyFlow || !isPeanutWallet const networkFee = useMemo(() => { if (isFeeEstimationError) return '-' @@ -113,7 +116,7 @@ export default function ConfirmPaymentView({ Sponsored by Peanut! ) - }, [estimatedGasCostUsd, isFeeEstimationError, isUsingExternalWallet]) + }, [estimatedGasCostUsd, isFeeEstimationError]) const { tokenIconUrl: sendingTokenIconUrl, @@ -121,9 +124,9 @@ export default function ConfirmPaymentView({ resolvedChainName: sendingResolvedChainName, resolvedTokenSymbol: sendingResolvedTokenSymbol, } = useTokenChainIcons({ - chainId: selectedChainID, - tokenAddress: selectedTokenData?.address, - tokenSymbol: selectedTokenData?.symbol, + chainId: isUsingExternalWallet ? selectedChainID : PEANUT_WALLET_CHAIN.id.toString(), + tokenAddress: isUsingExternalWallet ? selectedTokenData?.address : PEANUT_WALLET_TOKEN, + tokenSymbol: isUsingExternalWallet ? selectedTokenData?.symbol : PEANUT_WALLET_TOKEN_SYMBOL, }) const { @@ -147,7 +150,7 @@ export default function ConfirmPaymentView({ loadingStep ) ) - }, [isProcessing, loadingStep, isCalculatingFees, isEstimatingGas, isUsingExternalWallet]) + }, [isProcessing, loadingStep, isCalculatingFees, isEstimatingGas]) useEffect(() => { if (chargeIdFromUrl && !chargeDetails) { @@ -192,7 +195,6 @@ export default function ConfirmPaymentView({ selectedChainID, prepareTransactionDetails, isDirectUsdPayment, - isUsingExternalWallet, wagmiAddress, peanutWalletAddress, ]) @@ -227,7 +229,7 @@ export default function ConfirmPaymentView({ ) } return false - }, [chargeDetails, selectedTokenData, selectedChainID, isUsingExternalWallet]) + }, [chargeDetails, selectedTokenData, selectedChainID]) const routeTypeError = useMemo((): string | null => { if (!isCrossChainPayment || !xChainRoute || !isPeanutWallet) return null @@ -241,7 +243,7 @@ export default function ConfirmPaymentView({ routeObject: xChainRoute, }, }) - return 'No route found for this token pair. You can try with a different token pair, or try again later' + return ROUTE_NOT_FOUND_ERROR } return null @@ -398,14 +400,17 @@ export default function ConfirmPaymentView({ } const minReceived = useMemo(() => { - if (!xChainRoute || !chargeDetails?.tokenDecimals) return null - return formatUnits(BigInt(xChainRoute.rawResponse.route.estimate.toAmountMin), chargeDetails.tokenDecimals) - }, [xChainRoute, chargeDetails?.tokenDecimals]) + if (!xChainRoute || !chargeDetails?.tokenDecimals || !requestedResolvedTokenSymbol) return null + const amount = formatUnits( + BigInt(xChainRoute.rawResponse.route.estimate.toAmountMin), + chargeDetails.tokenDecimals + ) + return isStableCoin(requestedResolvedTokenSymbol) ? `$ ${amount}` : `${amount} ${requestedResolvedTokenSymbol}` + }, [xChainRoute, chargeDetails?.tokenDecimals, requestedResolvedTokenSymbol]) return (
-
{parsedPaymentData?.recipient && ( = ({ : transaction.currencySymbol || getDisplayCurrencySymbol(actualCurrencyCode) // Use provided sign+symbol or derive symbol let amountString = Math.abs(amount).toString() - if (transaction.currency?.code === 'USD') { + if (transaction.currency?.code === 'USD' && isStableCoin) { amountString = transaction.currency?.amount } // If it's a token and not USD/ARS, transaction.tokenSymbol should be displayed after amount. diff --git a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx index 57935d6ef..bfc99fd45 100644 --- a/src/components/TransactionDetails/TransactionDetailsDrawer.tsx +++ b/src/components/TransactionDetails/TransactionDetailsDrawer.tsx @@ -9,7 +9,7 @@ import { useWallet } from '@/hooks/wallet/useWallet' import { useUserStore } from '@/redux/hooks' import { chargesApi } from '@/services/charges' import { sendLinksApi } from '@/services/sendLinks' -import { formatAmount, formatDate, getInitialsFromName } from '@/utils' +import { formatAmount, formatDate, getInitialsFromName, isStableCoin } from '@/utils' import { formatIban } from '@/utils/general.utils' import { getDisplayCurrencySymbol } from '@/utils/currency' import { cancelOnramp } from '@/app/actions/onramp' @@ -168,9 +168,10 @@ export const TransactionDetailsReceipt = ({ amountDisplay = `${currencySymbol} ${formatAmount(Number(currencyAmount))}` } } else { - amountDisplay = transaction.currency?.amount - ? `$ ${formatAmount(Number(transaction.currency.amount))}` - : `$ ${formatAmount(transaction.amount as number)}` + amountDisplay = + transaction.currency?.amount && isStableCoin(transaction.tokenSymbol ?? '') + ? `$ ${formatAmount(Number(transaction.currency.amount))}` + : `$ ${formatAmount(transaction.amount as number)}` } const feeDisplay = transaction.fee !== undefined ? formatAmount(transaction.fee as number) : 'N/A' diff --git a/src/components/Withdraw/views/Confirm.withdraw.view.tsx b/src/components/Withdraw/views/Confirm.withdraw.view.tsx index ca2ea1397..89085bd85 100644 --- a/src/components/Withdraw/views/Confirm.withdraw.view.tsx +++ b/src/components/Withdraw/views/Confirm.withdraw.view.tsx @@ -10,11 +10,12 @@ import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow' import { useTokenChainIcons } from '@/hooks/useTokenChainIcons' import { ITokenPriceData } from '@/interfaces' -import { formatAmount } from '@/utils' +import { formatAmount, isStableCoin } from '@/utils' import { interfaces } from '@squirrel-labs/peanut-sdk' import { type PeanutCrossChainRoute } from '@/services/swap' import { useMemo, useState } from 'react' import { formatUnits } from 'viem' +import { ROUTE_NOT_FOUND_ERROR } from '@/constants' interface WithdrawConfirmViewProps { amount: string @@ -60,9 +61,10 @@ export default function ConfirmWithdrawView({ }) const minReceived = useMemo(() => { - if (!xChainRoute) return null - return formatUnits(BigInt(xChainRoute.rawResponse.route.estimate.toAmountMin), token.decimals) - }, [xChainRoute]) + if (!xChainRoute || !resolvedTokenSymbol) return null + const amount = formatUnits(BigInt(xChainRoute.rawResponse.route.estimate.toAmountMin), token.decimals) + return isStableCoin(resolvedTokenSymbol) ? `$ ${amount}` : `${amount} ${resolvedTokenSymbol}` + }, [xChainRoute, resolvedTokenSymbol]) const networkFeeDisplay = useMemo(() => { if (networkFee < 0.01) return 'Sponsored by Peanut!' @@ -98,7 +100,7 @@ export default function ConfirmWithdrawView({ setIsRouteExpired(true) }} disableTimerRefetch={isProcessing} - timerError={error?.includes('not available for withdraw') ? error : null} + timerError={error == ROUTE_NOT_FOUND_ERROR ? error : null} /> @@ -154,7 +156,7 @@ export default function ConfirmWithdrawView({ variant="purple" shadowSize="4" onClick={() => { - if (error.includes('not available for withdraw')) { + if (error === ROUTE_NOT_FOUND_ERROR) { onBack() } else if (isRouteExpired) { onRouteRefresh?.() diff --git a/src/constants/general.consts.ts b/src/constants/general.consts.ts index 19b30db97..09611c3f5 100644 --- a/src/constants/general.consts.ts +++ b/src/constants/general.consts.ts @@ -200,3 +200,6 @@ export const pathTitles: { [key: string]: string } = { } export const STABLE_COINS = ['USDC', 'USDT', 'DAI', 'BUSD'] + +export const ROUTE_NOT_FOUND_ERROR = + 'No route found for this token pair. You can try with a different token pair, or try again later' diff --git a/src/constants/zerodev.consts.ts b/src/constants/zerodev.consts.ts index 6653badab..8e86f8036 100644 --- a/src/constants/zerodev.consts.ts +++ b/src/constants/zerodev.consts.ts @@ -42,6 +42,7 @@ export const PEANUT_WALLET_SUPPORTED_TOKENS: Record = { */ export const USER_OP_ENTRY_POINT = getEntryPoint('0.7') export const ZERODEV_KERNEL_VERSION = KERNEL_V3_1 +export const USER_OPERATION_REVERT_REASON_TOPIC = '0x1c4fada7374c0a9ee8841fc38afe82932dc0f8e69012e927f061a8bae611a201' export const PUBLIC_CLIENTS_BY_CHAIN: Record< string, diff --git a/src/hooks/usePaymentInitiator.ts b/src/hooks/usePaymentInitiator.ts index 02f9d44f3..aba6a432b 100644 --- a/src/hooks/usePaymentInitiator.ts +++ b/src/hooks/usePaymentInitiator.ts @@ -21,7 +21,7 @@ import { TChargeTransactionType, TRequestChargeResponse, } from '@/services/services.types' -import { areEvmAddressesEqual, ErrorHandler, isNativeCurrency } from '@/utils' +import { areEvmAddressesEqual, ErrorHandler, isNativeCurrency, isTxReverted } from '@/utils' import { useAppKitAccount } from '@reown/appkit/react' import { peanut, interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' @@ -483,7 +483,7 @@ export const usePaymentInitiator = () => { console.error('sendTransactions returned invalid receipt (missing hash?):', receipt) throw new Error('Transaction likely failed or was not submitted correctly by the wallet.') } - if (receipt.status === 'reverted') { + if (isTxReverted(receipt)) { console.error('Transaction reverted according to receipt:', receipt) throw new Error(`Transaction failed (reverted). Hash: ${receipt.transactionHash}`) } @@ -572,7 +572,7 @@ export const usePaymentInitiator = () => { console.log(`Transaction ${i + 1} receipt:`, txReceipt) receipts.push(txReceipt) - if (txReceipt.status === 'reverted') { + if (isTxReverted(txReceipt)) { console.error(`Transaction ${i + 1} reverted:`, txReceipt) throw new Error(`Transaction ${i + 1} failed (reverted).`) } diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts index 12c1008d8..dc6a26ddd 100644 --- a/src/hooks/useWebSocket.ts +++ b/src/hooks/useWebSocket.ts @@ -93,7 +93,11 @@ export const useWebSocket = (options: UseWebSocketOptions = {}) => { } const handleHistoryEntry = (entry: HistoryEntry) => { - if (entry.type === 'DIRECT_SEND' && entry.status === 'NEW' && !entry.senderAccount) { + if ( + (entry.type === 'DIRECT_SEND' || entry.type === 'REQUEST') && + entry.status === 'NEW' && + !entry.senderAccount + ) { // Ignore pending requests from the server return } diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index 9b7586945..053aed85c 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -7,6 +7,7 @@ import { PINTA_WALLET_TOKEN_NAME, PINTA_WALLET_TOKEN_SYMBOL, STABLE_COINS, + USER_OPERATION_REVERT_REASON_TOPIC, } from '@/constants' import * as interfaces from '@/interfaces' import { AccountType } from '@/interfaces' @@ -1188,3 +1189,8 @@ export function isAndroid(): boolean { const userAgent = window.navigator.userAgent.toLowerCase() return /android/.test(userAgent) } + +export function isTxReverted(receipt: TransactionReceipt): boolean { + if (receipt.status === 'reverted') return true + return receipt.logs.some((log) => log.topics[0] === USER_OPERATION_REVERT_REASON_TOPIC) +}