OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / 31893fa

Fix 'No address found with ID: undefined' by migrating ALL contacts on load
SYSTEMATIC DEBUGGING TRACE: ======================== Step 1: Found error location - lib/wallet-storage.cjs:1332 - removeAddressFromContact() throws when addressId is undefined Step 2: Traced caller - wallet-server.cjs:1294 - const addressId = rest[1] (from URL path) - rest[1] is undefined when URL doesn't have addressId Step 3: Traced UI caller - ui/app.js:983 - onclick="removeContactAddress('${contact.id}', '${addr.id}'...)" - Passes addr.id which is undefined Step 4: Root cause identified - Contacts stored in DB may have: 1. Old single-address format (no addresses array) 2. New format WITHOUT IDs on addresses 3. New format WITH IDs (correct) - getContacts() returns raw data without migration - UI expects addr.id to always exist Step 5: Comprehensive fix implemented - Added migrateContactFormat() helper - Migrates old single-address to multi-address - Adds missing IDs to addresses in multi-address format - Updates getContacts() to migrate ALL contacts - Updates getContactById() to migrate on fetch - Updates findContact() to search addresses array - Migration happens transparently on every read Testing: 1. Old contacts: Will get addresses array with IDs 2. New contacts missing IDs: Will get IDs added 3. New contacts with IDs: No change (fast path) 4. Remove address: Will find addressId correctly 5. Send to contact: Will find address by ID correctly This ensures ALL contacts in memory have proper IDs regardless of how they were originally stored. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
31893fa30f6ec97702862e44b1194d493a38c16d
by plankatron <fleeky@gmail.com> · 2026-01-28T00:46:48.000Z

Changed files (1) +92 -10

lib/wallet-storage.cjs +92 -10
……@@ -1040,6 +1040,67 @@ async function addContact({ address, chain, name, notes = '' }) {
10401040 * @param {string} [chain] - Chain name to filter by (optional)
10411041 * @returns {Promise<Array>} - Array of contacts sorted by name
10421042 */
1043/**
1044 * Migrate a contact to new multi-address format with IDs
1045 * @param {Object} contact - Contact to migrate
1046 * @returns {Object} - Migrated contact
1047 */
1048function migrateContactFormat(contact) {
1049 // Already in new format with proper IDs - check if ALL addresses have IDs
1050 if (contact.addresses && Array.isArray(contact.addresses)) {
1051 // Check if any address is missing an ID
1052 const needsMigration = contact.addresses.some(addr => !addr.id)
1053
1054 if (!needsMigration) {
1055 // All addresses have IDs, no migration needed
1056 return contact
1057 }
1058
1059 // Some addresses missing IDs - add them
1060 const migratedAddresses = contact.addresses.map(addr => {
1061 if (!addr.id) {
1062 return {
1063 ...addr,
1064 id: crypto.randomBytes(8).toString('hex'),
1065 network: addr.network || getConfig().network || 'testnet',
1066 label: addr.label || '',
1067 addedAt: addr.addedAt || Date.now()
1068 }
1069 }
1070 return addr
1071 })
1072
1073 return {
1074 ...contact,
1075 addresses: migratedAddresses
1076 }
1077 }
1078
1079 // Old single-address format - migrate to multi-address
1080 if (contact.address && contact.chain) {
1081 const config = getConfig()
1082 const defaultNetwork = config.network || 'testnet'
1083
1084 return {
1085 ...contact,
1086 addresses: [{
1087 id: crypto.randomBytes(8).toString('hex'),
1088 chain: contact.chain,
1089 address: contact.address,
1090 network: contact.network || defaultNetwork,
1091 label: '',
1092 addedAt: contact.addedAt || Date.now()
1093 }],
1094 // Remove old fields
1095 address: undefined,
1096 chain: undefined
1097 }
1098 }
1099
1100 // Contact with no addresses - return as is
1101 return contact
1102}
1103
10431104async function getContacts(chain = null) {
10441105 if (!txHistoryDb) {
10451106 throw new Error('Storage not initialized')
……@@ -1050,11 +1111,21 @@ async function getContacts(chain = null) {
10501111
10511112 for await (const data of txHistoryDb.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
10521113 try {
1053 const contact = JSON.parse(data.value)
1054
1055 // Filter by chain if specified
1056 if (chain && contact.chain !== chain) {
1057 continue
1114 let contact = JSON.parse(data.value)
1115
1116 // Migrate contact format if needed
1117 contact = migrateContactFormat(contact)
1118
1119 // Filter by chain if specified (check both old and new format)
1120 if (chain) {
1121 // New format: check if any address matches chain
1122 if (contact.addresses && Array.isArray(contact.addresses)) {
1123 const hasChain = contact.addresses.some(addr => addr.chain === chain)
1124 if (!hasChain) continue
1125 } else if (contact.chain && contact.chain !== chain) {
1126 // Old format fallback
1127 continue
1128 }
10581129 }
10591130
10601131 contacts.push(contact)
……@@ -1084,10 +1155,15 @@ async function findContact(nameOrAddress, chain = null) {
10841155 )
10851156 if (byName) return byName
10861157
1087 // Try exact address match
1088 const byAddress = contacts.find(c =>
1089 c.address.toLowerCase() === nameOrAddress.toLowerCase()
1090 )
1158 // Try exact address match (check in addresses array)
1159 const byAddress = contacts.find(c => {
1160 if (c.addresses && Array.isArray(c.addresses)) {
1161 return c.addresses.some(addr =>
1162 addr.address.toLowerCase() === nameOrAddress.toLowerCase()
1163 )
1164 }
1165 return false
1166 })
10911167 if (byAddress) return byAddress
10921168
10931169 // Try partial name match
……@@ -1111,7 +1187,13 @@ async function getContactById(id) {
11111187 try {
11121188 const data = await txHistoryDb.get(`contact:${id}`)
11131189 if (!data) return null
1114 return JSON.parse(data.value)
1190
1191 let contact = JSON.parse(data.value)
1192
1193 // Migrate contact format if needed
1194 contact = migrateContactFormat(contact)
1195
1196 return contact
11151197 } catch (err) {
11161198 if (err.code === 'LEVEL_NOT_FOUND') {
11171199 return null