OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / 591bf62

Add ERC-20 token transaction sync support (fixes USDT not showing)
ROOT CAUSE: Blockscout's txlist endpoint only returns native ETH transactions. ERC-20 token transfers (like USDT) require the tokentx endpoint. Changes: 1. Add getTokenTxListUrl() and parseTokenResponse() to blockscout provider 2. Add fetchTokenTransactions() function for ERC-20 tokens 3. Update syncTransactionsFromWDK to detect token chains (ethereum-usdt) 4. Use fetchTokenTransactions() for tokens, fetchTransactions() for native ETH 5. Add USDT contract address for Sepolia testnet filtering This is why transactions appeared on Etherscan but not in the wallet! Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
591bf624bc0082043274a34b19cf0f68e4dcf07a
by plankatron <fleeky@gmail.com> · 2026-01-29T17:46:33.000Z

Changed files (2) +114 -4

lib/blockchain-providers.cjs +96 -0
……@@ -18,6 +18,16 @@ const ETHEREUM_PROVIDERS = {
1818 return `${baseUrl}?module=account&action=txlist&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc`
1919 },
2020
21 // NEW: Get ERC-20 token transaction list
22 getTokenTxListUrl (address, network, contractAddress = null) {
23 const baseUrl = network === 'testnet' ? this.testnetUrl : this.mainnetUrl
24 let url = `${baseUrl}?module=account&action=tokentx&address=${address}&startblock=0&endblock=latest&page=1&offset=100&sort=desc`
25 if (contractAddress) {
26 url += `&contractaddress=${contractAddress}`
27 }
28 return url
29 },
30
2131 parseResponse (data) {
2232 if (data.status === '1' && Array.isArray(data.result)) {
2333 return data.result.map(tx => ({
……@@ -32,6 +42,27 @@ const ETHEREUM_PROVIDERS = {
3242 }))
3343 }
3444 throw new Error('Invalid response format')
45 },
46
47 // NEW: Parse token transaction response
48 parseTokenResponse (data) {
49 if (data.status === '1' && Array.isArray(data.result)) {
50 return data.result.map(tx => ({
51 hash: tx.hash,
52 from: tx.from,
53 to: tx.to,
54 value: tx.value, // Token amount in smallest unit
55 tokenSymbol: tx.tokenSymbol,
56 tokenName: tx.tokenName,
57 tokenDecimal: parseInt(tx.tokenDecimal) || 0,
58 contractAddress: tx.contractAddress,
59 fee: tx.gasUsed && tx.gasPrice ? (BigInt(tx.gasUsed) * BigInt(tx.gasPrice)).toString() : '0',
60 blockNumber: parseInt(tx.blockNumber) || 0,
61 timestamp: parseInt(tx.timeStamp) || 0,
62 status: tx.txreceipt_status === '1' ? 'confirmed' : 'failed'
63 }))
64 }
65 throw new Error('Invalid response format')
3566 }
3667 },
3768
……@@ -268,6 +299,70 @@ async function fetchTransactions (chain, address, network, config) {
268299 throw new Error(`All providers failed. Last error: ${lastError?.message}`)
269300}
270301
302/**
303 * Fetch ERC-20 token transactions for an Ethereum address
304 * @param {string} address - Ethereum address
305 * @param {string} network - 'testnet' or 'mainnet'
306 * @param {Object} config - Configuration object from getConfig()
307 * @param {string} contractAddress - Optional: filter by token contract address
308 * @returns {Promise<Array>} - Array of token transactions
309 */
310async function fetchTokenTransactions (address, network, config, contractAddress = null) {
311 const providers = ETHEREUM_PROVIDERS
312
313 // Get primary provider from config
314 const primaryProviderName = config.blockchain.ethereum.provider
315 const apiKey = config.blockchain.ethereum.apiKey
316
317 // Build provider list (primary first, then fallbacks)
318 const providerList = [
319 primaryProviderName,
320 ...Object.keys(providers).filter(p => p !== primaryProviderName)
321 ]
322
323 let lastError = null
324
325 for (const providerName of providerList) {
326 const provider = providers[providerName]
327 if (!provider || !provider.getTokenTxListUrl) {
328 console.log(` ⚠️ ${providerName} doesn't support token transactions`)
329 continue
330 }
331
332 try {
333 // Skip if provider needs API key but we don't have one
334 if (provider.needsApiKey && !apiKey) {
335 console.log(` ⚠️ Skipping ${provider.name} (API key required)`)
336 continue
337 }
338
339 console.log(` 📡 Fetching token txs from ${provider.name}...`)
340
341 // Get token transaction list URL
342 const url = provider.getTokenTxListUrl(address, network, contractAddress)
343 const response = await fetch(url)
344
345 if (!response.ok) {
346 throw new Error(`HTTP ${response.status}: ${response.statusText}`)
347 }
348
349 const data = await response.json()
350 const transactions = provider.parseTokenResponse(data)
351
352 console.log(` ✅ ${provider.name}: ${transactions.length} token transactions`)
353 return transactions
354 } catch (err) {
355 console.log(` ⚠️ ${provider.name} failed: ${err.message}`)
356 lastError = err
357 continue
358 }
359 }
360
361 // All providers failed - return empty array instead of throwing
362 console.log(` ⚠️ All token tx providers failed: ${lastError?.message}`)
363 return []
364}
365
271366/**
272367 * Get list of available providers for a chain
273368 * @param {string} chain - 'bitcoin' or 'ethereum'
……@@ -293,6 +388,7 @@ module.exports = {
293388 ETHEREUM_PROVIDERS,
294389 BITCOIN_PROVIDERS,
295390 fetchTransactions,
391 fetchTokenTransactions,
296392 getAvailableProviders,
297393 getProviderInfo
298394}
lib/wallet-storage.cjs +18 -4
……@@ -13,7 +13,7 @@ const b4a = require('b4a')
1313const IdentityKey = require('keet-identity-key')
1414const { createPairingServer, createPairingClient } = require('./pairing-protocol.cjs')
1515const { loadWalletMnemonic, initializeWDK } = require('./wdk-integration.cjs')
16const { fetchTransactions } = require('./blockchain-providers.cjs')
16const { fetchTransactions, fetchTokenTransactions } = require('./blockchain-providers.cjs')
1717const { getConfig } = require('./config.cjs')
1818
1919// Storage configuration
……@@ -925,9 +925,23 @@ async function syncTransactionsFromWDK(wdk, config = {}) {
925925 const config = getConfig()
926926 const network = config.network || 'testnet'
927927
928 // Fetch transactions using blockchain-providers.cjs
929 // Use base chain for provider (ethereum, not ethereum-usdt)
930 const providerTxs = await fetchTransactions(baseChain, currentAddress, network, config)
928 // Determine if this is a token (USDT) or native ETH
929 const isToken = chain.includes('-usdt')
930 let providerTxs = []
931
932 if (isToken) {
933 // Fetch ERC-20 token transactions (USDT)
934 console.log(` 📡 Fetching token transactions for ${chain}...`)
935 // USDT contract address on Sepolia testnet
936 const USDT_CONTRACT = network === 'testnet'
937 ? '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06'
938 : null // TODO: Add mainnet USDT contract
939 providerTxs = await fetchTokenTransactions(currentAddress, network, config, USDT_CONTRACT)
940 } else {
941 // Fetch native ETH transactions
942 console.log(` 📡 Fetching ETH transactions for ${chain}...`)
943 providerTxs = await fetchTransactions(baseChain, currentAddress, network, config)
944 }
931945
932946 for (const tx of providerTxs) {
933947 // Check if already stored