From 3a2bf02dc78073daa47690ad0888bb13a16fe87d Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Tue, 18 Nov 2025 15:33:45 +0000 Subject: [PATCH 1/5] Check for lowercase addresses for duplicates --- src/zustand/tokens.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zustand/tokens.ts b/src/zustand/tokens.ts index 3238bc6..a7593f3 100644 --- a/src/zustand/tokens.ts +++ b/src/zustand/tokens.ts @@ -118,10 +118,12 @@ export const tokensStore = createStore( const tokens = { ...state.tokens } for (const tokenAddress of tokenAddresses) { - const exists = (tokens[serializedKey] || []).some( - (x) => x.address === tokenAddress, + const existingToken = (tokens[serializedKey] || []).find( + (x) => x.address.toLowerCase() === tokenAddress.toLowerCase(), ) - if (!exists) + // Only add if token doesn't exist yet + // If it exists but is hidden, respect the user's choice and don't modify it + if (!existingToken) tokens[serializedKey] = [ { address: tokenAddress, From f12b80e7cde10683cf888d81aca207d5fcb3fa3f Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Tue, 18 Nov 2025 15:41:14 +0000 Subject: [PATCH 2/5] Autodetect tokens --- src/actions/getAccountTokens.ts | 88 ++++++++++++++++++++------------- src/hooks/useAccountTokens.ts | 14 +++++- src/screens/account-details.tsx | 12 ++++- src/screens/token-transfer.tsx | 25 ++++++++++ 4 files changed, 102 insertions(+), 37 deletions(-) diff --git a/src/actions/getAccountTokens.ts b/src/actions/getAccountTokens.ts index 7157a7b..11833a2 100644 --- a/src/actions/getAccountTokens.ts +++ b/src/actions/getAccountTokens.ts @@ -1,9 +1,7 @@ -import { type Address, parseAbiItem } from 'abitype' +import type { Address } from 'abitype' import type { GetLogsParameters } from 'viem' import { erc20Abi } from 'viem' -import { getLogsQueryOptions } from '~/hooks/useGetLogs' -import { queryClient } from '~/react-query' import type { Client } from '~/viem' export async function getAccountTokens( @@ -18,40 +16,55 @@ export async function getAccountTokens( toBlock: GetLogsParameters['toBlock'] }, ) { - console.log('getAccountTokens params:', { address, fromBlock, toBlock }) + const effectiveFromBlock = 0n + const effectiveToBlock = toBlock ?? 'latest' + + console.log('Effective blocks:', { + effectiveFromBlock, + effectiveToBlock, + originalFromBlock: fromBlock, + }) + + // Transfer event signature hash (keccak256 of "Transfer(address,address,uint256)") + const transferEventSignature = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' as const + + // Pad address to 32 bytes for topic matching + const paddedAddress = `0x000000000000000000000000${address + .slice(2) + .toLowerCase()}` as const const [transfersFrom, transfersTo] = await Promise.all([ - queryClient.fetchQuery( - getLogsQueryOptions(client, { - event: parseAbiItem( - 'event Transfer(address indexed from, address indexed to, uint256)', - ), - args: { - from: address, - }, - fromBlock, - toBlock, + // Transfers FROM this address: topic[1] = address + client + .getLogs({ + fromBlock: effectiveFromBlock, + toBlock: effectiveToBlock, + topics: [transferEventSignature, paddedAddress], + } as any) // Type assertion needed for raw topics API + .catch((err) => { + console.error('Error fetching transfersFrom:', err) + return [] }), - ), - queryClient.fetchQuery( - getLogsQueryOptions(client, { - event: parseAbiItem( - 'event Transfer(address indexed from, address indexed to, uint256)', - ), - args: { - to: address, - }, - fromBlock, - toBlock, + // Transfers TO this address: topic[2] = address + client + .getLogs({ + fromBlock: effectiveFromBlock, + toBlock: effectiveToBlock, + topics: [transferEventSignature, null, paddedAddress], + } as any) // Type assertion needed for raw topics API + .catch((err) => { + console.error('Error fetching transfersTo:', err) + return [] }), - ), ]) - // console.log(transfersFrom, transfersTo, 'transfers'); + + const relevantTransfers = [...transfersFrom, ...transfersTo] + const potentialTokens = [ - ...new Set([ - ...(transfersFrom?.map((t) => t.address) || []), - ...(transfersTo?.map((t) => t.address) || []), - ]), + ...new Set( + relevantTransfers.map((t) => t.address.toLowerCase() as Address), + ), ] const erc20Tokens = await Promise.all( @@ -62,10 +75,15 @@ export async function getAccountTokens( abi: erc20Abi, functionName: 'decimals', }) - return typeof decimals === 'number' && decimals >= 0 && decimals <= 255 - ? tokenAddress - : null - } catch { + const isValid = + typeof decimals === 'number' && decimals >= 0 && decimals <= 255 + if (!isValid) { + console.warn( + `Token ${tokenAddress} has invalid decimals: ${decimals}. Skipping.`, + ) + } + return isValid ? tokenAddress : null + } catch (_err) { return null } }), diff --git a/src/hooks/useAccountTokens.ts b/src/hooks/useAccountTokens.ts index e04c91f..e414740 100644 --- a/src/hooks/useAccountTokens.ts +++ b/src/hooks/useAccountTokens.ts @@ -29,6 +29,9 @@ export function useAccountTokensQueryOptions(args: UseAccountTokensParameters) { return queryOptions({ enabled: Boolean(address && network.forkBlockNumber), queryKey: getAccountTokensQueryKey([client.key, address, stringify(args)]), + // Refetch tokens periodically to detect new transfers + refetchInterval: 3000, + staleTime: 2000, async queryFn() { if (!address) throw new Error('address is required') const tokens = await getAccountTokens(client, { @@ -110,7 +113,16 @@ export function useAccountTokens({ address }: UseAccountTokensParameters) { const tokens = useMemo(() => { if (!tokensKey) return [] - return tokensStore.tokens[tokensKey] ?? [] + const tokensList = tokensStore.tokens[tokensKey] ?? [] + + // Deduplicate tokens by address (case-insensitive) + const seen = new Set() + return tokensList.filter((token) => { + const lowerAddress = token.address.toLowerCase() + if (seen.has(lowerAddress)) return false + seen.add(lowerAddress) + return true + }) }, [tokensKey, tokensStore.tokens]) return Object.assign(useQuery(queryOptions), { diff --git a/src/screens/account-details.tsx b/src/screens/account-details.tsx index 32bea7b..b285c5d 100644 --- a/src/screens/account-details.tsx +++ b/src/screens/account-details.tsx @@ -205,7 +205,7 @@ function TokenRow({ accountAddress, tokenAddress }: TokenRowProps) { address: accountAddress, }) - const { removeToken } = useAccountTokens({ + const { removeToken, hideToken } = useAccountTokens({ address: accountAddress, }) @@ -282,6 +282,16 @@ function TokenRow({ accountAddress, tokenAddress }: TokenRowProps) { text={tokenAddress!} variant="ghost primary" /> + { + e.stopPropagation() + hideToken({ tokenAddress: tokenAddress! }) + }} + /> )} diff --git a/src/screens/token-transfer.tsx b/src/screens/token-transfer.tsx index 3482fc8..c9ac8dd 100644 --- a/src/screens/token-transfer.tsx +++ b/src/screens/token-transfer.tsx @@ -25,11 +25,13 @@ import { Stack, Text, } from '~/design-system' +// import { getAccountTokensQueryKey } from '~/hooks/useAccountTokens' import { useBalance } from '~/hooks/useBalance' import { useClient } from '~/hooks/useClient' import { useErc20Balance } from '~/hooks/useErc20Balance' import { useErc20Metadata } from '~/hooks/useErc20Metadata' import { useWriteContract } from '~/hooks/useWriteContract' +import { queryClient } from '~/react-query' type TransferFormData = { recipient: string @@ -113,6 +115,29 @@ export default function TokenTransfer() { if (receipt.status === 'success') { toast.success(`Transfer successful! TX: ${txHash.slice(0, 10)}...`) + + // Invalidate account tokens query to refetch and detect new tokens + console.log('Invalidating account tokens query for:', accountAddress) + await queryClient.invalidateQueries({ + predicate: (query) => { + const matches = query.queryKey[0] === 'account-tokens' + if (matches) { + console.log( + 'Found account-tokens query to invalidate:', + query.queryKey, + ) + } + return matches + }, + }) + + // Also invalidate balances since they changed + await queryClient.invalidateQueries({ + predicate: (query) => + query.queryKey[0] === 'balance' || + query.queryKey[0] === 'erc20-balance', + }) + navigate(-1) } else { toast.error('Transaction reverted') From 9ab52da99048d6165d33e4f77951163982b09ed6 Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Tue, 18 Nov 2025 15:44:18 +0000 Subject: [PATCH 3/5] Up version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ad68186..01112bc 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "devwallet", - "version": "0.2.0", + "version": "0.3.0", "private": true, "extension": { "name": "DW: DevWallet", - "description": "Dev Wallet for EVM & Forge" + "description": "DevWallet helps your Solidity development workflow. Works great with Forge" }, "scripts": { "anvil": "anvil --fork-url https://cloudflare-eth.com", From b444e0f36e3097eb13cc508b181804a804bb49e0 Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Tue, 18 Nov 2025 15:45:37 +0000 Subject: [PATCH 4/5] Remove commented out import --- src/screens/token-transfer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/screens/token-transfer.tsx b/src/screens/token-transfer.tsx index c9ac8dd..ba506fb 100644 --- a/src/screens/token-transfer.tsx +++ b/src/screens/token-transfer.tsx @@ -25,7 +25,6 @@ import { Stack, Text, } from '~/design-system' -// import { getAccountTokensQueryKey } from '~/hooks/useAccountTokens' import { useBalance } from '~/hooks/useBalance' import { useClient } from '~/hooks/useClient' import { useErc20Balance } from '~/hooks/useErc20Balance' From 994604326d41aa18478a53960a23c94bb87c63e1 Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Tue, 18 Nov 2025 15:50:21 +0000 Subject: [PATCH 5/5] Comments --- src/actions/getAccountTokens.ts | 8 +------- src/screens/token-transfer.tsx | 35 +++++---------------------------- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/src/actions/getAccountTokens.ts b/src/actions/getAccountTokens.ts index 11833a2..daf2d89 100644 --- a/src/actions/getAccountTokens.ts +++ b/src/actions/getAccountTokens.ts @@ -18,13 +18,7 @@ export async function getAccountTokens( ) { const effectiveFromBlock = 0n const effectiveToBlock = toBlock ?? 'latest' - - console.log('Effective blocks:', { - effectiveFromBlock, - effectiveToBlock, - originalFromBlock: fromBlock, - }) - + console.log(fromBlock) // Transfer event signature hash (keccak256 of "Transfer(address,address,uint256)") const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' as const diff --git a/src/screens/token-transfer.tsx b/src/screens/token-transfer.tsx index ba506fb..fbb8271 100644 --- a/src/screens/token-transfer.tsx +++ b/src/screens/token-transfer.tsx @@ -2,29 +2,13 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' import { toast } from 'sonner' -import { - type Address, - type BaseError, - type Hex, - formatUnits, - isAddress, - parseUnits, -} from 'viem' +import { type Address, type BaseError, formatUnits, type Hex, isAddress, parseUnits, } from 'viem' import { Container } from '~/components' import * as Form from '~/components/form' import { Spinner } from '~/components/svgs' import { erc20Abi } from '~/constants/abi' -import { - Box, - Button, - Column, - Columns, - Inline, - Separator, - Stack, - Text, -} from '~/design-system' +import { Box, Button, Column, Columns, Inline, Separator, Stack, Text, } from '~/design-system' import { useBalance } from '~/hooks/useBalance' import { useClient } from '~/hooks/useClient' import { useErc20Balance } from '~/hooks/useErc20Balance' @@ -114,23 +98,14 @@ export default function TokenTransfer() { if (receipt.status === 'success') { toast.success(`Transfer successful! TX: ${txHash.slice(0, 10)}...`) - - // Invalidate account tokens query to refetch and detect new tokens - console.log('Invalidating account tokens query for:', accountAddress) + // invalidate tokens to fetch new ones await queryClient.invalidateQueries({ predicate: (query) => { - const matches = query.queryKey[0] === 'account-tokens' - if (matches) { - console.log( - 'Found account-tokens query to invalidate:', - query.queryKey, - ) - } - return matches + return query.queryKey[0] === 'account-tokens' }, }) - // Also invalidate balances since they changed + // invalidate balances to update await queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'balance' ||