OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop commits / e928e5a

Fix USDT address selection bug - QR codes and receive addresses now use correct chain
CRITICAL BUG FIX: Token chains (ethereum-usdt) were incorrectly using the base chain's (ethereum) current address instead of their own current address when generating QR codes and displaying receive addresses. Changes: - wallet-server.cjs:463 - Fix /api/address/ to use chainParam not chainName - wallet-server.cjs:482 - Fix fallback storage to use chainParam - wallet-server.cjs:687 - Fix /api/qrcode/ to use chainParam not chainName - wallet-server.cjs:406 - Fix /api/balance/ to use chainParam not baseChain Impact: - Users can now properly select which address to use for USDT - QR codes display the correct address matching the dropdown selection - Each token chain maintains independent current address tracking Root Cause: Storage uses full chain names (ethereum-usdt) but code was incorrectly using base chain names (ethereum) for getCurrentAddress() lookups. Testing Required: 1. Select USDT, derive multiple addresses 2. Change current address via dropdown 3. Verify QR code updates to match selection See docs/development/2026-01-29-address-selection-bug.md for full analysis. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
e928e5a61f04301c025939adef3f9979807b0d45
by plankatron <fleeky@gmail.com> · 2026-01-29T13:53:32.000Z

Changed files (3) +554 -4

docs/development/2026-01-29-address-selection-bug.md +343 -0
……@@ -0,0 +1,343 @@
1# Code Review Findings: hyper-wallet-desktop
2
3**Date:** 2026-01-29
4**Reviewer:** Claude Sonnet 4.5
5**Focus Areas:** QR code generation for USDT, address selection/display
6
7---
8
9## Critical Bugs Found
10
11### Bug #1: QR Code Shows Wrong Address for Token Chains (USDT)
12
13**Location:** `wallet-server.cjs:666-689` (GET /api/qrcode/)
14
15**Severity:** HIGH - Users may receive funds to wrong address
16
17**Description:**
18When generating a QR code for token chains like `ethereum-usdt`, the code incorrectly retrieves the current address for the base chain (`ethereum`) instead of the token chain (`ethereum-usdt`).
19
20**Root Cause:**
21```javascript
22// Line 679: Gets base chain (ethereum-usdt -> ethereum)
23const chainName = getBaseChain(chainParam)
24
25// Line 684: BUG - Uses base chain to get current address
26const currentAddr = await getCurrentAddress(chainName) // Gets 'ethereum' current
27```
28
29**Issue:**
30- Addresses are stored separately per chain in the database (including `ethereum`, `ethereum-usdt`, etc.)
31- When you change the current address for USDT via the dropdown selector, it updates the current address for `ethereum-usdt` in storage
32- But when generating the QR code for USDT, it retrieves the current address for `ethereum`
33- This creates a mismatch: the QR shows Ethereum's current address, not USDT's current address
34
35**User Impact:**
361. User selects USDT in the wallet
372. User changes the receive address using the dropdown (e.g., from Address 0 to Address 1)
383. The UI shows Address 1 is selected
394. The QR code still shows Address 0 (because it's using Ethereum's current address)
405. User receives USDT to the wrong address
41
42**Fix Required:**
43```javascript
44// wallet-server.cjs line 666-689
45'GET /api/qrcode/': requireAuth(async (req, res, chain) => {
46 try {
47 const walletMnemonic = loadWalletMnemonicCached()
48 if (!walletMnemonic) {
49 return sendError(res, 'Wallet not initialized', 400)
50 }
51
52 const network = chain.startsWith('testnet') ? 'testnet' : 'mainnet'
53 const chainParam = chain.replace(/^(testnet|mainnet)-/, '')
54
55 // For tokens, get the base chain address (ethereum-usdt -> ethereum)
56 const chainName = getBaseChain(chainParam)
57
58 // Get current address index from storage (defaults to 0)
59 const identity = await loadIdentity()
60 await initializeStorage(identity)
61
62 // FIX: Use original chainParam for storage lookup, not chainName
63 const currentAddr = await getCurrentAddress(chainParam) // Changed from chainName
64 const accountIndex = currentAddr ? currentAddr.index : 0
65
66 // Use chainName for WDK account (correct - USDT uses ETH addresses)
67 const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
68 const account = await wdk.getAccount(chainName, accountIndex) // This is correct
69 const address = await account.getAddress()
70
71 // ... rest of function
72 }
73})
74```
75
76---
77
78### Bug #2: Receive Modal Shows Wrong Address for Token Chains
79
80**Location:** `wallet-server.cjs:443-507` (GET /api/address/)
81
82**Severity:** HIGH - Same issue as Bug #1
83
84**Description:**
85The `/api/address/:chain` endpoint has the exact same bug - it uses the base chain to retrieve the current address instead of the original chain parameter.
86
87**Code:**
88```javascript
89// Line 456: Gets base chain
90const chainName = getBaseChain(chainParam)
91
92// Line 461: BUG - Uses base chain to get current address
93const currentAddr = await getCurrentAddress(chainName) // Should use chainParam
94```
95
96**Fix Required:**
97```javascript
98// wallet-server.cjs line 443-507
99'GET /api/address/': requireAuth(async (req, res, chain) => {
100 try {
101 const walletMnemonic = loadWalletMnemonicCached()
102 if (!walletMnemonic) {
103 return sendError(res, 'Wallet not initialized', 400)
104 }
105
106 const network = chain.startsWith('testnet') ? 'testnet' : 'mainnet'
107 const chainParam = chain.replace(/^(testnet|mainnet)-/, '')
108
109 // For tokens, get the base chain address (ethereum-usdt -> ethereum)
110 const chainName = getBaseChain(chainParam)
111
112 // Check storage for current address first
113 const identity = await loadIdentity()
114 await initializeStorage(identity)
115
116 // FIX: Use original chainParam for storage lookup
117 const currentAddr = await getCurrentAddress(chainParam) // Changed from chainName
118
119 if (currentAddr) {
120 return sendJSON(res, {
121 chain: chainName, // Return base chain name for display
122 network,
123 address: currentAddr.address,
124 index: currentAddr.index,
125 label: currentAddr.label || ''
126 })
127 }
128
129 // ... rest of function (default address generation)
130 }
131})
132```
133
134---
135
136### Bug #3: Balance Endpoint May Have Same Issue
137
138**Location:** `wallet-server.cjs:400-436` (GET /api/balance/)
139
140**Severity:** MEDIUM - May show balance for wrong address
141
142**Description:**
143The balance endpoint for token chains also uses `getBaseChain()` to get the current address:
144
145```javascript
146// Line 403: Gets base chain
147const baseChain = getBaseChain(chainParam)
148
149// Line 405: Uses base chain for current address
150const currentAddr = await getCurrentAddress(baseChain) // Should use chainParam
151```
152
153**Verification Needed:**
154This may be intentional if token balances are queried at the account level (not address level), but it should be verified.
155
156**Fix (if needed):**
157```javascript
158// Use chainParam instead of baseChain for getCurrentAddress()
159const currentAddr = await getCurrentAddress(chainParam) // Changed from baseChain
160```
161
162---
163
164## Technical Analysis
165
166### Address Storage Architecture
167
168The wallet stores addresses separately per chain:
169- Storage keys: `wallet:address:${chain}:${network}:${index}`
170- Chains are stored separately: `ethereum`, `ethereum-usdt`, `bitcoin`, etc.
171- Each chain tracks its own "current" address via the `isCurrent` flag
172
173### How getCurrentAddress Works
174
175```javascript
176// lib/wallet-storage.cjs:423-430
177async function getCurrentAddress(chain) {
178 const addresses = await getAddresses(chain)
179 if (addresses.length === 0) return null
180
181 // Find address marked as current, or return latest
182 const current = addresses.find(a => a.isCurrent)
183 return current || addresses[addresses.length - 1]
184}
185```
186
187This function is **chain-specific** - it only looks at addresses for the given chain.
188
189### Why getBaseChain Exists
190
191The `getBaseChain()` function exists because:
192- USDT is an ERC-20 token on Ethereum
193- USDT uses the same address format as Ethereum (0x...)
194- WDK operations need to use the base chain (`ethereum`) to generate addresses
195- But storage operations should use the full chain name (`ethereum-usdt`)
196
197### The Bug Pattern
198
199**Correct pattern:**
200```javascript
201const chainName = getBaseChain(chainParam) // For WDK operations
202const currentAddr = await getCurrentAddress(chainParam) // For storage operations
203const account = await wdk.getAccount(chainName, accountIndex) // WDK uses base chain
204```
205
206**Current (buggy) pattern:**
207```javascript
208const chainName = getBaseChain(chainParam)
209const currentAddr = await getCurrentAddress(chainName) // BUG: Should use chainParam
210const account = await wdk.getAccount(chainName, accountIndex)
211```
212
213---
214
215## Affected User Flows
216
217### Scenario 1: Switching Between Ethereum and USDT
2181. User has Address 0 and Address 1 for both Ethereum and USDT
2192. User selects Ethereum, changes to Address 1 (current for ethereum = Address 1)
2203. User switches to USDT, changes to Address 0 (current for ethereum-usdt = Address 0)
2214. QR code shows Address 1 (ethereum's current) instead of Address 0 (ethereum-usdt's current)
222
223### Scenario 2: Receive Modal
2241. User opens "Receive USDT" modal
2252. Address dropdown shows correct addresses for USDT with correct "current" marker
2263. But the displayed address in the modal is from Ethereum's current address
2274. User confusion: dropdown says Address 0 is current, but modal shows Address 1
228
229---
230
231## Recommended Fixes
232
233### Priority 1: Fix /api/qrcode/ and /api/address/ endpoints
234
235**File:** `wallet-server.cjs`
236
237**Changes:**
2381. Line 461: Change `getCurrentAddress(chainName)` to `getCurrentAddress(chainParam)`
2392. Line 684: Change `getCurrentAddress(chainName)` to `getCurrentAddress(chainParam)`
240
241### Priority 2: Verify /api/balance/ endpoint
242
243**File:** `wallet-server.cjs`
244
245**Investigation needed:**
246- Does token balance query need per-address lookup or account-level?
247- If per-address, change line 405: `getCurrentAddress(baseChain)` to `getCurrentAddress(chainParam)`
248
249### Priority 3: Add storage layer validation
250
251**File:** `lib/wallet-storage.cjs`
252
253**Enhancement:**
254Add validation in `getCurrentAddress()` to ensure it handles token chains correctly:
255
256```javascript
257async function getCurrentAddress(chain) {
258 // Validate chain parameter is the full chain name (not base chain)
259 if (chain !== chain.toLowerCase()) {
260 console.warn(`getCurrentAddress called with non-lowercase chain: ${chain}`)
261 chain = chain.toLowerCase()
262 }
263
264 const addresses = await getAddresses(chain)
265 if (addresses.length === 0) return null
266
267 const current = addresses.find(a => a.isCurrent)
268 return current || addresses[addresses.length - 1]
269}
270```
271
272---
273
274## Testing Recommendations
275
276### Test Case 1: USDT Address Selection
2771. Create wallet with testnet USDT
2782. Derive 2 addresses for USDT
2793. Set Address 0 as current
2804. Verify QR code shows Address 0
2815. Change to Address 1 via dropdown
2826. Verify QR code now shows Address 1 (currently fails)
283
284### Test Case 2: Cross-Chain Independence
2851. Create addresses for both Ethereum and USDT
2862. Set Ethereum current = Address 1
2873. Set USDT current = Address 0
2884. Switch to Ethereum view → verify QR shows Address 1
2895. Switch to USDT view → verify QR shows Address 0 (currently fails - shows Address 1)
290
291### Test Case 3: Receive Modal Consistency
2921. Open "Receive USDT" modal
2932. Verify address in input field matches address marked as current in dropdown
2943. Change address in dropdown
2954. Verify address in input field updates accordingly (currently fails)
296
297---
298
299## Additional Observations
300
301### Storage Keys
302
303The address storage uses chain-specific keys:
304```javascript
305// lib/wallet-storage.cjs:358
306const key = `wallet:address:${chain}:${network}:${index}`
307```
308
309This confirms that `ethereum` and `ethereum-usdt` addresses are stored separately.
310
311### WDK Integration
312
313The WDK integration correctly handles base chain mapping:
314```javascript
315// lib/wdk-integration.cjs:171-177
316function getBaseChain(chain) {
317 if (!isTokenChain(chain)) {
318 return chain
319 }
320 // Extract base chain from 'ethereum-usdt' -> 'ethereum'
321 return chain.split('-')[0]
322}
323```
324
325This is working as designed - the bug is in how this is used.
326
327---
328
329## Conclusion
330
331**Summary:**
332Two critical bugs cause QR codes and receive addresses to show the wrong address when using token chains like USDT. The bugs stem from using the base chain (`ethereum`) for storage lookups when the original chain parameter (`ethereum-usdt`) should be used.
333
334**Impact:**
335Users may receive funds to unintended addresses, leading to confusion and potential loss of funds if they don't control all derived addresses.
336
337**Recommended Action:**
338Apply the fixes to `/api/qrcode/` and `/api/address/` endpoints immediately, then run comprehensive tests before deploying.
339
340**Estimated Fix Time:**
341- Code changes: 10 minutes
342- Testing: 30 minutes
343- Total: < 1 hour
docs/development/2026-01-29-address-selection-fix-applied.md +204 -0
……@@ -0,0 +1,204 @@
1# Address Selection Bug - Fix Applied
2
3**Date:** 2026-01-29
4**Fixed By:** Claude Sonnet 4.5
5**Related:** See [2026-01-29-address-selection-bug.md](./2026-01-29-address-selection-bug.md) for full analysis
6
7---
8
9## Summary
10
11Fixed critical bugs where QR codes and receive addresses showed the wrong address when using token chains (USDT). The bug caused the system to use Ethereum's current address instead of USDT's current address when looking up from storage.
12
13---
14
15## Changes Made
16
17### Fix #1: GET /api/address/ endpoint
18**File:** `wallet-server.cjs`
19**Line:** 463
20
21**Before:**
22```javascript
23const currentAddr = await getCurrentAddress(chainName)
24```
25
26**After:**
27```javascript
28// FIX: Use chainParam (ethereum-usdt) not chainName (ethereum) for storage lookup
29const currentAddr = await getCurrentAddress(chainParam)
30```
31
32**Also fixed fallback storage** (Line 482):
33```javascript
34// Before
35chain: chainName,
36
37// After
38chain: chainParam, // FIX: Store with original chain name (ethereum-usdt)
39```
40
41---
42
43### Fix #2: GET /api/qrcode/ endpoint
44**File:** `wallet-server.cjs`
45**Line:** 687
46
47**Before:**
48```javascript
49const currentAddr = await getCurrentAddress(chainName)
50```
51
52**After:**
53```javascript
54// FIX: Use chainParam (ethereum-usdt) not chainName (ethereum) for storage lookup
55const currentAddr = await getCurrentAddress(chainParam)
56```
57
58---
59
60### Fix #3: GET /api/balance/ endpoint
61**File:** `wallet-server.cjs`
62**Line:** 406
63
64**Before:**
65```javascript
66const currentAddr = await getCurrentAddress(baseChain)
67```
68
69**After:**
70```javascript
71// FIX: Use chainParam (ethereum-usdt) not baseChain (ethereum) for storage lookup
72const currentAddr = await getCurrentAddress(chainParam)
73```
74
75---
76
77## Technical Explanation
78
79### The Pattern
80
81**Correct usage:**
82```javascript
83const chainName = getBaseChain(chainParam) // For WDK operations (ethereum)
84const currentAddr = await getCurrentAddress(chainParam) // For storage (ethereum-usdt)
85const account = await wdk.getAccount(chainName, accountIndex) // WDK uses base chain
86```
87
88### Why This Works
89
901. **Storage Layer**: Addresses are stored separately per chain
91 - Key: `wallet:address:ethereum-usdt:testnet:0`
92 - Each chain (`ethereum`, `ethereum-usdt`) tracks its own "current" address
93
942. **WDK Layer**: Token operations use the base chain
95 - USDT uses Ethereum addresses (0x...)
96 - WDK account is created with `ethereum` chain name
97 - But we use the address index from `ethereum-usdt` storage
98
993. **Result**: Correct address for the selected token chain
100
101---
102
103## Testing Required
104
105### Test Case 1: USDT Address Selection
106```bash
107# Steps:
1081. Start wallet-server.cjs
1092. Create/restore wallet
1103. Select USDT (ethereum-usdt)
1114. Derive 2 addresses
1125. Change current address from dropdown
1136. Verify QR code updates to match selected address
114
115# Expected: QR code shows Address 1
116# Previously: QR code showed Address 0 (from ethereum chain)
117```
118
119### Test Case 2: Cross-Chain Independence
120```bash
121# Steps:
1221. Derive addresses for both Ethereum and USDT
1232. Set Ethereum current = Address 1
1243. Set USDT current = Address 0
1254. Switch to Ethereum → verify QR shows Address 1
1265. Switch to USDT → verify QR shows Address 0
127
128# Expected: Each chain uses its own current address
129# Previously: USDT used Ethereum's current address
130```
131
132### Test Case 3: Receive Modal
133```bash
134# Steps:
1351. Open "Receive USDT" modal
1362. Change address in dropdown
1373. Verify displayed address matches dropdown selection
138
139# Expected: Address field updates when dropdown changes
140# Previously: Address field showed wrong address
141```
142
143---
144
145## Affected Endpoints
146
147### Fixed
148- `GET /api/address/:chain` - Returns current receive address
149- `GET /api/qrcode/:chain` - Generates QR code for receive address
150- `GET /api/balance/:chain` - Gets balance for current address
151
152### Unchanged (Already Correct)
153- `GET /api/addresses/:chain` - Lists all addresses (uses chainParam)
154- `POST /api/addresses/:chain/derive` - Derives new address (uses chainParam)
155- `POST /api/addresses/:chain/:index/current` - Sets current address (uses chainParam)
156
157---
158
159## Verification
160
161### Before Fix
162```javascript
163// User selects USDT
164// Sets Address 1 as current for ethereum-usdt
165// QR endpoint does:
166getCurrentAddress('ethereum') // Returns ethereum's Address 0
167// QR shows wrong address!
168```
169
170### After Fix
171```javascript
172// User selects USDT
173// Sets Address 1 as current for ethereum-usdt
174// QR endpoint does:
175getCurrentAddress('ethereum-usdt') // Returns ethereum-usdt's Address 1
176// QR shows correct address!
177```
178
179---
180
181## Deployment Notes
182
1831. **No Database Migration Needed**: Storage structure unchanged
1842. **Backward Compatible**: Existing addresses continue to work
1853. **No API Changes**: Same endpoints, same parameters
1864. **Restart Required**: Server must be restarted to apply fix
187
188---
189
190## Next Steps
191
1921. Restart wallet-server.cjs
1932. Run test cases above
1943. Verify with real USDT testnet transactions
1954. Update CHANGELOG.md with fix details
1965. Consider adding unit tests for getCurrentAddress() logic
197
198---
199
200## Files Modified
201
202- `wallet-server.cjs` - 4 changes across 3 endpoints
203- `docs/development/2026-01-29-address-selection-bug.md` - Analysis document
204- `docs/development/2026-01-29-address-selection-fix-applied.md` - This file
wallet-server.cjs +7 -4
……@@ -402,7 +402,8 @@ const routes = {
402402 const tokenInfo = getTokenInfo(chainParam, network === 'testnet')
403403 const baseChain = getBaseChain(chainParam)
404404
405 const currentAddr = await getCurrentAddress(baseChain)
405 // FIX: Use chainParam (ethereum-usdt) not baseChain (ethereum) for storage lookup
406 const currentAddr = await getCurrentAddress(chainParam)
406407 const accountIndex = currentAddr ? currentAddr.index : 0
407408
408409 const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
……@@ -458,7 +459,8 @@ const routes = {
458459 // Check storage for current address first
459460 const identity = await loadIdentity()
460461 await initializeStorage(identity)
461 const currentAddr = await getCurrentAddress(chainName)
462 // FIX: Use chainParam (ethereum-usdt) not chainName (ethereum) for storage lookup
463 const currentAddr = await getCurrentAddress(chainParam)
462464
463465 if (currentAddr) {
464466 return sendJSON(res, {
……@@ -477,7 +479,7 @@ const routes = {
477479
478480 // Store the first address automatically
479481 const addressData = {
480 chain: chainName,
482 chain: chainParam, // FIX: Store with original chain name (ethereum-usdt)
481483 network,
482484 address,
483485 path: `m/44'/${chainName === 'bitcoin' ? 0 : 60}'/0'/0/0`,
……@@ -681,7 +683,8 @@ const routes = {
681683 // Get current address index from storage (defaults to 0)
682684 const identity = await loadIdentity()
683685 await initializeStorage(identity)
684 const currentAddr = await getCurrentAddress(chainName)
686 // FIX: Use chainParam (ethereum-usdt) not chainName (ethereum) for storage lookup
687 const currentAddr = await getCurrentAddress(chainParam)
685688 const accountIndex = currentAddr ? currentAddr.index : 0
686689
687690 const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })