Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b54a5e7
feat: abstract squid route fetching
jjramirezn Jun 12, 2025
769b541
refactor: use parseunits
jjramirezn Jun 12, 2025
5076156
refactor: remove console.dir
jjramirezn Jun 12, 2025
65eb664
feat: handle very large numbers with careful scaling
jjramirezn Jun 12, 2025
49451d1
refactor: use const for squid api url
jjramirezn Jun 13, 2025
37a248c
feat: add cross-chain action card
jjramirezn Jun 18, 2025
526c84d
Merge remote-tracking branch 'origin/peanut-wallet' into feat/cross-c…
jjramirezn Jun 18, 2025
9c138db
refactor: use decimals from token price instead of hardcoded
jjramirezn Jun 18, 2025
0704210
style: Apply prettier formatting
jjramirezn Jun 18, 2025
67dfe8d
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/cor…
jjramirezn Jun 18, 2025
74a01ba
feat: show min received for cross-chain payments
jjramirezn Jun 22, 2025
4eccc76
chore: clean unused variables
jjramirezn Jun 22, 2025
97cdbcf
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/cro…
jjramirezn Jun 30, 2025
9d96d70
Merge remote-tracking branch 'origin/peanut-wallet-dev' into feat/cor…
jjramirezn Jun 30, 2025
186d490
Merge branch 'feat/coral-integration' into feat/cross-chain-action-card
jjramirezn Jun 30, 2025
83a5ac8
fix: pr comments
jjramirezn Jun 30, 2025
df106b3
fix: withdrawData and pulsate animation
jjramirezn Jun 30, 2025
19aab4e
fix: delete unused code
jjramirezn Jun 30, 2025
90d6233
refactor: better comments for direct usd payments
jjramirezn Jul 1, 2025
8c1bcdf
Merge pull request #919 from peanutprotocol/feat/cross-chain-action-card
jjramirezn Jul 1, 2025
ad2fe8e
Merge pull request #919 from peanutprotocol/feat/cross-chain-action-card
jjramirezn Jul 1, 2025
923ddb3
feat: add qr scanning for xchain
jjramirezn Jul 1, 2025
d0cc9a5
Merge branch 'feat/qr-scanning-xchain' into feat/coral-integration
jjramirezn Jul 1, 2025
cca8489
fix: remove malicious code
jjramirezn Jul 1, 2025
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
26 changes: 3 additions & 23 deletions src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@ import ErrorAlert from '@/components/Global/ErrorAlert'
import NavHeader from '@/components/Global/NavHeader'
import PeanutActionDetailsCard from '@/components/Global/PeanutActionDetailsCard'
import { PaymentInfoRow } from '@/components/Payment/PaymentInfoRow'
import {
PEANUT_WALLET_CHAIN,
PEANUT_WALLET_TOKEN,
PEANUT_WALLET_TOKEN_DECIMALS,
PEANUT_WALLET_TOKEN_SYMBOL,
} from '@/constants'
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 { shortenAddressLong } from '@/utils/general.utils'
import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'
import DirectSuccessView from '@/components/Payment/Views/Status.payment.view'
import { ErrorHandler, getBridgeChainName } from '@/utils'
import { getOfframpCurrencyConfig } from '@/utils/bridge.utils'
Expand All @@ -33,7 +26,7 @@ type View = 'INITIAL' | 'SUCCESS'
export default function WithdrawBankPage() {
const { amountToWithdraw, selectedBankAccount: bankAccount, error, setError } = useWithdrawFlow()
const { user, fetchUser } = useAuth()
const { address, sendTransactions } = useWallet()
const { address, sendMoney } = useWallet()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [view, setView] = useState<View>('INITIAL')
Expand Down Expand Up @@ -136,20 +129,7 @@ export default function WithdrawBankPage() {
}

// Step 2: prepare and send the transaction from peanut wallet to the deposit address
const amountToSend = parseUnits(createPayload.amount, PEANUT_WALLET_TOKEN_DECIMALS)

const txData = encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [data.depositInstructions.toAddress as `0x${string}`, amountToSend],
})

const transaction: peanutInterfaces.IPeanutUnsignedTransaction = {
to: PEANUT_WALLET_TOKEN,
data: txData,
}

const receipt = await sendTransactions([transaction], PEANUT_WALLET_CHAIN.id.toString())
const receipt = await sendMoney(data.depositInstructions.toAddress as `0x${string}`, createPayload.amount)

if (receipt.status === 'reverted') {
throw new Error('Transaction reverted by the network.')
Expand Down
133 changes: 105 additions & 28 deletions src/app/(mobile-ui)/withdraw/crypto/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ConfirmWithdrawView from '@/components/Withdraw/views/Confirm.withdraw.vi
import InitialWithdrawView from '@/components/Withdraw/views/Initial.withdraw.view'
import { useWithdrawFlow, WithdrawData } from '@/context/WithdrawFlowContext'
import { InitiatePaymentPayload, usePaymentInitiator } from '@/hooks/usePaymentInitiator'
import { useWallet } from '@/hooks/wallet/useWallet'
import { useAppDispatch, usePaymentStore } from '@/redux/hooks'
import { paymentActions } from '@/redux/slices/payment-slice'
import { chargesApi } from '@/services/charges'
Expand All @@ -21,13 +22,15 @@ 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 { useRouter } from 'next/navigation'
import { useCallback, useEffect } from 'react'
import { useCallback, useEffect, useMemo } from 'react'

export default function WithdrawCryptoPage() {
const router = useRouter()
const dispatch = useAppDispatch()
const { chargeDetails: activeChargeDetailsFromStore } = usePaymentStore()
const { isConnected: isPeanutWallet } = useWallet()
const {
amountToWithdraw,
setAmountToWithdraw,
Expand All @@ -47,18 +50,33 @@ export default function WithdrawCryptoPage() {
initiatePayment,
isProcessing,
error: paymentErrorFromHook,
feeCalculations,
prepareTransactionDetails,
xChainRoute,
isCalculatingFees,
isPreparingTx,
} = usePaymentInitiator()

// Helper to manage errors consistently
const setError = useCallback(
(error: string | null) => {
setPaymentError(error)
dispatch(paymentActions.setError(error))
},
[setPaymentError, dispatch]
)

const clearErrors = useCallback(() => {
setError(null)
}, [setError])

useEffect(() => {
if (!amountToWithdraw) {
console.error('Amount not available in WithdrawFlowContext for withdrawal, redirecting.')
router.push('/withdraw')
return
}
dispatch(paymentActions.setChargeDetails(null))
setPaymentError(null)
clearErrors()
setCurrentView('INITIAL')
}, [amountToWithdraw, router, dispatch, setAmountToWithdraw, setCurrentView])

Expand All @@ -68,26 +86,31 @@ export default function WithdrawCryptoPage() {

useEffect(() => {
if (currentView === 'CONFIRM' && activeChargeDetailsFromStore && withdrawData) {
prepareTransactionDetails(activeChargeDetailsFromStore, false)
console.log('Preparing withdraw transaction details...')
console.dir(activeChargeDetailsFromStore)
Comment on lines +89 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove or improve console.log statements

Multiple console.log statements are present that should be removed or converted to a more appropriate logging mechanism for production code.

Either remove these console statements or use a proper logging library with appropriate log levels:

-            console.log('Preparing withdraw transaction details...')
-            console.dir(activeChargeDetailsFromStore)
+            // Log at debug level if needed
+            // logger.debug('Preparing withdraw transaction details...', activeChargeDetailsFromStore)
-        console.log('Refreshing withdraw route due to expiry...')
-        console.log('About to call prepareTransactionDetails with:', activeChargeDetailsFromStore)
+        // logger.debug('Refreshing withdraw route due to expiry...', { activeChargeDetailsFromStore })
-        console.log('Cross-chain check:', {
-            fromChainId,
-            toChainId,
-            isPeanutWallet,
-            isCrossChain: fromChainId !== toChainId,
-        })
+        // logger.debug('Cross-chain check', { fromChainId, toChainId, isPeanutWallet, isCrossChain: fromChainId !== toChainId })

Also applies to: 241-242, 266-272

🤖 Prompt for AI Agents
In src/app/(mobile-ui)/withdraw/crypto/page.tsx around lines 89-90, 241-242, and
266-272, there are console.log statements used for debugging. These should be
removed or replaced with a proper logging library that supports log levels
suitable for production. Identify all console.log and console.dir calls in these
lines and either delete them or refactor them to use a configured logger with
appropriate severity levels like info or debug.

prepareTransactionDetails(
activeChargeDetailsFromStore,
PEANUT_WALLET_TOKEN,
PEANUT_WALLET_CHAIN.id.toString(),
amountToWithdraw
)
}
}, [currentView, activeChargeDetailsFromStore, withdrawData, prepareTransactionDetails])
}, [currentView, activeChargeDetailsFromStore, withdrawData, prepareTransactionDetails, amountToWithdraw])

const handleSetupReview = useCallback(
async (data: Omit<WithdrawData, 'amount'>) => {
if (!amountToWithdraw) {
console.error('Amount to withdraw is not set or not available from context')
setPaymentError('Withdrawal amount is missing.')
setError('Withdrawal amount is missing.')
return
}
const completeWithdrawData = { ...data, amount: amountToWithdraw }
setWithdrawData(completeWithdrawData)

setIsPreparingReview(true)
setPaymentError(null)
dispatch(paymentActions.setError(null))
clearErrors()
dispatch(paymentActions.setChargeDetails(null))

try {
const completeWithdrawData = { ...data, amount: amountToWithdraw }
setWithdrawData(completeWithdrawData)
const apiRequestPayload: CreateRequestPayloadServices = {
recipientAddress: completeWithdrawData.address,
chainId: completeWithdrawData.chain.chainId.toString(),
Expand All @@ -97,9 +120,9 @@ export default function WithdrawCryptoPage() {
? peanutInterfaces.EPeanutLinkType.native
: peanutInterfaces.EPeanutLinkType.erc20
),
tokenAmount: amountToWithdraw,
tokenDecimals: completeWithdrawData.token.decimals.toString(),
tokenSymbol: completeWithdrawData.token.symbol,
tokenDecimals: String(completeWithdrawData.token.decimals),
tokenAmount: completeWithdrawData.amount,
}
const newRequest: TRequestResponse = await requestsApi.create(apiRequestPayload)

Expand All @@ -109,7 +132,7 @@ export default function WithdrawCryptoPage() {

const chargePayload: CreateChargeRequest = {
pricing_type: 'fixed_price',
local_price: { amount: completeWithdrawData.amount, currency: 'USD' },
local_price: { amount: completeWithdrawData.amount || amountToWithdraw, currency: 'USD' },
baseUrl: window.location.origin,
requestId: newRequest.uuid,
requestProps: {
Expand Down Expand Up @@ -139,8 +162,7 @@ export default function WithdrawCryptoPage() {
} catch (err: any) {
console.error('Error during setup review (request/charge creation):', err)
const errorMessage = err.message || 'Could not prepare withdrawal. Please try again.'
setPaymentError(errorMessage)
dispatch(paymentActions.setError(errorMessage))
setError(errorMessage)
} finally {
setIsPreparingReview(false)
}
Expand All @@ -154,18 +176,18 @@ export default function WithdrawCryptoPage() {
setCurrentView('CONFIRM')
} else {
console.error('Proceeding to confirm, but charge details or withdraw data are missing.')
setPaymentError('Failed to load withdrawal details for confirmation. Please go back and try again.')
setError('Failed to load withdrawal details for confirmation. Please go back and try again.')
}
}, [activeChargeDetailsFromStore, withdrawData, setCurrentView])

const handleConfirmWithdrawal = useCallback(async () => {
if (!activeChargeDetailsFromStore || !withdrawData || !amountToWithdraw) {
console.error('Withdraw data, active charge details, or amount missing for final confirmation')
setPaymentError('Essential withdrawal information is missing.')
setError('Essential withdrawal information is missing.')
return
}

setPaymentError(null)
clearErrors()
dispatch(paymentActions.setError(null))

const paymentPayload: InitiatePaymentPayload = {
Expand All @@ -191,17 +213,15 @@ export default function WithdrawCryptoPage() {
setCurrentView('INITIAL')

// clear any errors
setPaymentError(null)
dispatch(paymentActions.setError(null))
clearErrors()

// clear charge details
dispatch(paymentActions.setChargeDetails(null))
}, 3000) // wait 3 seconds to show success status before resetting
} else {
console.error('Withdrawal execution failed:', result.error)
const errMsg = result.error || 'Withdrawal processing failed.'
setPaymentError(errMsg)
dispatch(paymentActions.setError(errMsg))
setError(errMsg)
}
}, [
activeChargeDetailsFromStore,
Expand All @@ -215,15 +235,66 @@ export default function WithdrawCryptoPage() {
setPaymentError,
])

const handleRouteRefresh = useCallback(async () => {
if (!activeChargeDetailsFromStore) return
console.log('Refreshing withdraw route due to expiry...')
console.log('About to call prepareTransactionDetails with:', activeChargeDetailsFromStore)
await prepareTransactionDetails(
activeChargeDetailsFromStore,
PEANUT_WALLET_TOKEN,
PEANUT_WALLET_CHAIN.id.toString(),
amountToWithdraw
)
}, [activeChargeDetailsFromStore, prepareTransactionDetails, amountToWithdraw])

const handleBackFromConfirm = useCallback(() => {
setCurrentView('INITIAL')
setPaymentError(null)
clearErrors()
dispatch(paymentActions.setError(null))
dispatch(paymentActions.setChargeDetails(null))
}, [dispatch, setCurrentView])

const displayError = paymentError
const confirmButtonDisabled = !activeChargeDetailsFromStore || isProcessing
// Check if this is a cross-chain withdrawal (align with usePaymentInitiator logic)
const isCrossChainWithdrawal = useMemo<boolean>(() => {
if (!withdrawData || !activeChargeDetailsFromStore) return false

// In withdraw flow, we're moving from Peanut Wallet to the selected chain
// This matches the logic in usePaymentInitiator for withdraw flows
const fromChainId = isPeanutWallet ? PEANUT_WALLET_CHAIN.id.toString() : withdrawData.chain.chainId
const toChainId = activeChargeDetailsFromStore.chainId

console.log('Cross-chain check:', {
fromChainId,
toChainId,
isPeanutWallet,
isCrossChain: fromChainId !== toChainId,
})

return fromChainId !== toChainId
}, [withdrawData, activeChargeDetailsFromStore, isPeanutWallet])

// Check for route type errors (similar to payment flow)
const routeTypeError = useMemo<string | null>(() => {
if (!isCrossChainWithdrawal || !xChainRoute || !isPeanutWallet) return null

// For peanut wallet flows, only RFQ routes are allowed
if (xChainRoute.type === 'swap') {
return 'This token pair is not available for withdraw.'
}

return null
}, [isCrossChainWithdrawal, xChainRoute, isPeanutWallet])

// Display payment errors first (user actions), then route errors (system limitations)
const displayError = paymentError ?? routeTypeError

// Get network fee from route or fallback
const networkFee = useCallback(() => {
if (xChainRoute?.feeCostsUsd) {
return xChainRoute.feeCostsUsd < 0.01 ? '$ <0.01' : `$ ${xChainRoute.feeCostsUsd.toFixed(2)}`
}
return '$ 0.00'
}, [xChainRoute])

if (!amountToWithdraw) {
return <PeanutLoading />
Expand All @@ -248,9 +319,15 @@ export default function WithdrawCryptoPage() {
toAddress={withdrawData.address}
onConfirm={handleConfirmWithdrawal}
onBack={handleBackFromConfirm}
isProcessing={confirmButtonDisabled}
isProcessing={isProcessing}
error={displayError}
networkFee={feeCalculations?.estimatedFee}
networkFee={networkFee()}
// Timer props for cross-chain withdrawals
isCrossChain={isCrossChainWithdrawal}
routeExpiry={xChainRoute?.expiry}
isRouteLoading={isCalculatingFees || isPreparingTx}
onRouteRefresh={handleRouteRefresh}
xChainRoute={xChainRoute ?? undefined}
/>
)}

Expand Down
3 changes: 2 additions & 1 deletion src/app/[...recipient]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
<InitialPaymentView
{...(parsedPaymentData as ParsedURL)}
isAddMoneyFlow={isAddMoneyFlow}
isDirectPay={isDirectPay}
isDirectUsdPayment={isDirectPay}
currency={
currencyCode
? {
Expand All @@ -410,6 +410,7 @@ export default function PaymentPage({ recipient, flow = 'request_pay' }: Props)
isPintaReq={parsedPaymentData?.token?.symbol === 'PNT'}
currencyAmount={currencyCode && currencyAmount ? `${currencySymbol} ${currencyAmount}` : undefined}
isAddMoneyFlow={isAddMoneyFlow}
isDirectUsdPayment={isDirectPay}
/>
)}
{currentView === 'STATUS' && (
Expand Down
26 changes: 10 additions & 16 deletions src/app/actions/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,16 @@ export type FeeOptions = {
error?: string | null
}

export const getPublicClient = unstable_cache(
async (chainId: ChainId): Promise<PublicClient> => {
let client: PublicClient | undefined = PUBLIC_CLIENTS_BY_CHAIN[chainId]?.client
if (client) return client
const chain: Chain = extractChain({ chains: allChains, id: chainId })
if (!chain) throw new Error(`No chain found for chainId ${chainId}`)
return createPublicClient({
transport: http(infuraRpcUrls[chainId]),
chain,
})
},
['getPublicClient'],
{
tags: ['getPublicClient'],
}
)
export const getPublicClient = async (chainId: ChainId): Promise<PublicClient> => {
let client: PublicClient | undefined = PUBLIC_CLIENTS_BY_CHAIN[chainId]?.client
if (client) return client
const chain: Chain = extractChain({ chains: allChains, id: chainId })
if (!chain) throw new Error(`No chain found for chainId ${chainId}`)
return createPublicClient({
transport: http(infuraRpcUrls[chainId]),
chain,
})
}

export type PreparedTx = {
account: Hash
Expand Down
Loading
Loading