Skip to content
Draft
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
84 changes: 82 additions & 2 deletions src/hooks/wallet/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useAppDispatch, useWalletStore } from '@/redux/hooks'
import { walletActions } from '@/redux/slices/wallet-slice'
import { formatAmount } from '@/utils'
import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useRef } from 'react'
import type { Hex, Address } from 'viem'
import { erc20Abi, formatUnits, parseUnits, encodeFunctionData, getAddress } from 'viem'
import { useZeroDev } from '../useZeroDev'
Expand All @@ -24,6 +24,9 @@ export const useWallet = () => {
const [isFetchingBalance, setIsFetchingBalance] = useState(true)
const [isFetchingRewardBalance, setIsFetchingRewardBalance] = useState(true)
const { balance } = useWalletStore()
const eventListenerRef = useRef<(() => void) | null>(null)
const retryTimerRef = useRef<NodeJS.Timeout | null>(null)
const TIMEOUT_INTERVAL = 2000

Comment on lines +27 to 30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix timer ref type for browser env (Next.js client)
NodeJS.Timeout is incorrect in the browser and can cause TS errors. Use ReturnType<typeof setTimeout> for universal correctness.

-    const retryTimerRef = useRef<NodeJS.Timeout | null>(null)
+    const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const eventListenerRef = useRef<(() => void) | null>(null)
const retryTimerRef = useRef<NodeJS.Timeout | null>(null)
const TIMEOUT_INTERVAL = 2000
const eventListenerRef = useRef<(() => void) | null>(null)
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const TIMEOUT_INTERVAL = 2000
🤖 Prompt for AI Agents
In src/hooks/wallet/useWallet.ts around lines 27 to 30, the retryTimerRef is
typed as NodeJS.Timeout which is incorrect for browser (Next.js client) and can
cause TS errors; change its type to useRef<ReturnType<typeof setTimeout> | null>
so it works in both Node and browser environments, update any usages that clear
or assign the timer to remain compatible (clearTimeout on the ref.current) and
keep eventListenerRef and TIMEOUT_INTERVAL unchanged.

const sendMoney = useCallback(
async (toAddress: Address, amountInUsd: string) => {
Expand Down Expand Up @@ -103,11 +106,88 @@ export const useWallet = () => {
})
}, [address, dispatch])

// Set up real-time balance monitoring via contract events
const setupBalanceMonitoring = useCallback(() => {
if (!address) return

// Clean up previous event listener and retry timer
eventListenerRef.current?.()
if (retryTimerRef.current) {
clearTimeout(retryTimerRef.current)
retryTimerRef.current = null
}

try {
// Create two separate watchers for incoming and outgoing transfers
// Watch for incoming transfers (to: address)
const watchIncoming = peanutPublicClient.watchContractEvent({
address: PEANUT_WALLET_TOKEN,
abi: erc20Abi,
eventName: 'Transfer',
args: {
to: address as `0x${string}`,
},
onLogs: () => {
fetchBalance()
},
onError: (error) => {
console.error('Contract event listener error (incoming):', error)
if (retryTimerRef.current) {
clearTimeout(retryTimerRef.current)
}
retryTimerRef.current = setTimeout(() => setupBalanceMonitoring(), TIMEOUT_INTERVAL)
},
})

// Watch for outgoing transfers (from: address)
const watchOutgoing = peanutPublicClient.watchContractEvent({
address: PEANUT_WALLET_TOKEN,
abi: erc20Abi,
eventName: 'Transfer',
args: {
from: address as `0x${string}`,
},
onLogs: () => {
fetchBalance()
},
onError: (error) => {
console.error('Contract event listener error (outgoing):', error)
if (retryTimerRef.current) {
clearTimeout(retryTimerRef.current)
}
retryTimerRef.current = setTimeout(() => setupBalanceMonitoring(), TIMEOUT_INTERVAL)
},
})

// Store cleanup function that cleans up both watchers
eventListenerRef.current = () => {
watchIncoming()
watchOutgoing()
}
} catch (error) {
console.error('Failed to setup balance monitoring:', error)
}
}, [address, fetchBalance])
Comment on lines +123 to +170
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: this is not too efficient with credits

Some back of napkin calculations:

viem's watchContractEvent does the following:

2025-09-02_12-41

Which in infura (ignoring alchemy for now) it has these costs:

2025-09-02_12-42

Lets assume a session of 1 minute, that would be 30 calls to getFilterChanges (with the 2sec interval)

30 calls x 140 credits = 4200 credits
4200 credits + 80 credits (newFilter) = 4280 credits
4280 credits * 2 watchContractEvent = 8560 credits per user session
This becomes too much too quick (longer sessions, etc etc) only 100 sessions like this and we are at almost 1M credits (we have 15M per day right now)

I strongly recommend doing this with websocket triggered from the deposit indexer.


useEffect(() => {
if (!address) return

// Initial balance fetch
fetchBalance()
getRewardWalletBalance()
}, [address, fetchBalance, getRewardWalletBalance])

// Setup real-time monitoring
setupBalanceMonitoring()

// Cleanup on unmount or address change
return () => {
eventListenerRef.current?.()
if (retryTimerRef.current) {
clearTimeout(retryTimerRef.current)
retryTimerRef.current = null
}
}
}, [address, fetchBalance, getRewardWalletBalance, setupBalanceMonitoring])

return {
address: address!,
Expand Down
Loading