Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.')
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/(mobile-ui)/withdraw/crypto/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions src/components/Claim/Link/Initial.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
39 changes: 22 additions & 17 deletions src/components/Payment/Views/Confirm.payment.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ 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'
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'

Expand Down Expand Up @@ -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<string | React.ReactNode>(() => {
if (isFeeEstimationError) return '-'
Expand All @@ -113,17 +116,17 @@ export default function ConfirmPaymentView({
<span className="font-medium text-gray-500">Sponsored by Peanut!</span>
</>
)
}, [estimatedGasCostUsd, isFeeEstimationError, isUsingExternalWallet])
}, [estimatedGasCostUsd, isFeeEstimationError])

const {
tokenIconUrl: sendingTokenIconUrl,
chainIconUrl: sendingChainIconUrl,
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 {
Expand All @@ -147,7 +150,7 @@ export default function ConfirmPaymentView({
loadingStep
)
)
}, [isProcessing, loadingStep, isCalculatingFees, isEstimatingGas, isUsingExternalWallet])
}, [isProcessing, loadingStep, isCalculatingFees, isEstimatingGas])

useEffect(() => {
if (chargeIdFromUrl && !chargeDetails) {
Expand Down Expand Up @@ -192,7 +195,6 @@ export default function ConfirmPaymentView({
selectedChainID,
prepareTransactionDetails,
isDirectUsdPayment,
isUsingExternalWallet,
wagmiAddress,
peanutWalletAddress,
])
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -398,14 +400,17 @@ export default function ConfirmPaymentView({
}

const minReceived = useMemo<string | null>(() => {
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 (
<div className="flex min-h-[inherit] flex-col justify-between gap-8">
<NavHeader title={isAddMoneyFlow ? 'Add Money' : 'Send'} onPrev={handleGoBack} />

<div className="my-auto flex h-full flex-col justify-center space-y-4 pb-5">
{parsedPaymentData?.recipient && (
<PeanutActionDetailsCard
Expand Down
2 changes: 1 addition & 1 deletion src/components/TransactionDetails/TransactionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const TransactionCard: React.FC<TransactionCardProps> = ({
: 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'

Expand Down
14 changes: 8 additions & 6 deletions src/components/Withdraw/views/Confirm.withdraw.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,9 +61,10 @@ export default function ConfirmWithdrawView({
})

const minReceived = useMemo<string | null>(() => {
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<string | React.ReactNode>(() => {
if (networkFee < 0.01) return 'Sponsored by Peanut!'
Expand Down Expand Up @@ -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}
/>

<Card className="rounded-sm">
Expand Down Expand Up @@ -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?.()
Expand Down
3 changes: 3 additions & 0 deletions src/constants/general.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
1 change: 1 addition & 0 deletions src/constants/zerodev.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const PEANUT_WALLET_SUPPORTED_TOKENS: Record<string, string[]> = {
*/
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,
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/usePaymentInitiator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}`)
}
Expand Down Expand Up @@ -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).`)
}
Expand Down
6 changes: 5 additions & 1 deletion src/hooks/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions src/utils/general.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
Loading