| … | … | @@ -368,8 +368,11 @@ async function storeAddress(addressData) { |
| 368 | 368 | address: addressData.address, |
| 369 | 369 | path: addressData.path, |
| 370 | 370 | index: addressData.index, |
| 371 | | createdAt: Date.now(), |
| 372 | | device: getCurrentDevice()?.name || 'unknown' |
| 371 | label: addressData.label || '', // Pet-name/label for address |
| 372 | createdAt: addressData.createdAt || Date.now(), |
| 373 | device: getCurrentDevice()?.name || 'unknown', |
| 374 | isArchived: addressData.isArchived || false, // For hiding old addresses |
| 375 | isCurrent: addressData.isCurrent || false // Mark current receiving address |
| 373 | 376 | } |
| 374 | 377 | } |
| 375 | 378 | |
| … | … | @@ -420,7 +423,91 @@ async function getNextAddressIndex(chain) { |
| 420 | 423 | async function getCurrentAddress(chain) { |
| 421 | 424 | const addresses = await getAddresses(chain) |
| 422 | 425 | if (addresses.length === 0) return null |
| 423 | | return addresses[addresses.length - 1] |
| 426 | |
| 427 | // Find address marked as current, or return latest |
| 428 | const current = addresses.find(a => a.isCurrent) |
| 429 | return current || addresses[addresses.length - 1] |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Update address label |
| 434 | * @param {string} chain - 'bitcoin' or 'ethereum' |
| 435 | * @param {number} index - Address index |
| 436 | * @param {string} label - New label/pet-name |
| 437 | */ |
| 438 | async function updateAddressLabel(chain, index, label) { |
| 439 | if (!autobase) { |
| 440 | throw new Error('Storage not initialized') |
| 441 | } |
| 442 | |
| 443 | // Get existing address |
| 444 | const addresses = await getAddresses(chain) |
| 445 | const existing = addresses.find(a => a.index === index) |
| 446 | if (!existing) { |
| 447 | throw new Error(`Address not found: ${chain}:${index}`) |
| 448 | } |
| 449 | |
| 450 | // Update with new label |
| 451 | await storeAddress({ |
| 452 | ...existing, |
| 453 | label |
| 454 | }) |
| 455 | |
| 456 | return { ...existing, label } |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Set address as current receiving address |
| 461 | * @param {string} chain - 'bitcoin' or 'ethereum' |
| 462 | * @param {number} index - Address index to set as current |
| 463 | */ |
| 464 | async function setCurrentAddress(chain, index) { |
| 465 | if (!autobase) { |
| 466 | throw new Error('Storage not initialized') |
| 467 | } |
| 468 | |
| 469 | const addresses = await getAddresses(chain) |
| 470 | |
| 471 | // Find the address to set as current |
| 472 | const targetAddr = addresses.find(a => a.index === index) |
| 473 | if (!targetAddr) { |
| 474 | throw new Error(`Address not found: ${chain}:${index}`) |
| 475 | } |
| 476 | |
| 477 | // Update all addresses: unset current on others, set on target |
| 478 | for (const addr of addresses) { |
| 479 | if (addr.index === index) { |
| 480 | await storeAddress({ ...addr, isCurrent: true }) |
| 481 | } else if (addr.isCurrent) { |
| 482 | await storeAddress({ ...addr, isCurrent: false }) |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | return { ...targetAddr, isCurrent: true } |
| 487 | } |
| 488 | |
| 489 | /** |
| 490 | * Archive address (hide from UI) |
| 491 | * @param {string} chain - 'bitcoin' or 'ethereum' |
| 492 | * @param {number} index - Address index |
| 493 | */ |
| 494 | async function archiveAddress(chain, index) { |
| 495 | if (!autobase) { |
| 496 | throw new Error('Storage not initialized') |
| 497 | } |
| 498 | |
| 499 | const addresses = await getAddresses(chain) |
| 500 | const existing = addresses.find(a => a.index === index) |
| 501 | if (!existing) { |
| 502 | throw new Error(`Address not found: ${chain}:${index}`) |
| 503 | } |
| 504 | |
| 505 | await storeAddress({ |
| 506 | ...existing, |
| 507 | isArchived: true |
| 508 | }) |
| 509 | |
| 510 | return { ...existing, isArchived: true } |
| 424 | 511 | } |
| 425 | 512 | |
| 426 | 513 | // ============================================================================ |
| … | … | @@ -752,8 +839,10 @@ async function syncTransactionsFromWDK(wdk, config = {}) { |
| 752 | 839 | // Chain-specific transaction fetching |
| 753 | 840 | if (chain === 'bitcoin') { |
| 754 | 841 | // Bitcoin: Use Electrum client directly |
| 755 | | if (!account._electrumClient) { |
| 756 | | results.errors.push(`${chain}: No Electrum client available`) |
| 842 | if (!account._electrumClient || typeof account._electrumClient.blockchainScripthash_getHistory !== 'function') { |
| 843 | // Electrum client not available or method doesn't exist |
| 844 | // TODO: Use blockchain explorer API (Blockstream) instead |
| 845 | results.errors.push(`${chain}: Electrum client unavailable - Bitcoin transaction sync temporarily disabled`) |
| 757 | 846 | continue |
| 758 | 847 | } |
| 759 | 848 | |
| … | … | @@ -1117,15 +1206,16 @@ async function findContactByAddress(address, chain) { |
| 1117 | 1206 | } |
| 1118 | 1207 | |
| 1119 | 1208 | /** |
| 1120 | | * Add an address to an existing contact |
| 1209 | * Add an address to an existing contact (supports multiple addresses per chain) |
| 1121 | 1210 | * @param {string} contactId - Contact ID |
| 1122 | 1211 | * @param {Object} addressInfo - Address details |
| 1123 | | * @param {string} addressInfo.chain - Chain name (bitcoin, ethereum) |
| 1212 | * @param {string} addressInfo.chain - Chain name (bitcoin, ethereum, polygon, etc.) |
| 1124 | 1213 | * @param {string} addressInfo.address - Blockchain address |
| 1125 | | * @param {string} addressInfo.network - Network (testnet, mainnet) |
| 1214 | * @param {string} addressInfo.network - Network (testnet, mainnet, sepolia, etc.) |
| 1215 | * @param {string} [addressInfo.label] - Optional label (e.g., "Hardware wallet", "Hot wallet") |
| 1126 | 1216 | * @returns {Promise<Object>} - Updated contact |
| 1127 | 1217 | */ |
| 1128 | | async function addAddressToContact(contactId, { chain, address, network }) { |
| 1218 | async function addAddressToContact(contactId, { chain, address, network, label = '' }) { |
| 1129 | 1219 | if (!autobase) { |
| 1130 | 1220 | throw new Error('Storage not initialized') |
| 1131 | 1221 | } |
| … | … | @@ -1140,24 +1230,36 @@ async function addAddressToContact(contactId, { chain, address, network }) { |
| 1140 | 1230 | if (existing.addresses && Array.isArray(existing.addresses)) { |
| 1141 | 1231 | addresses = existing.addresses |
| 1142 | 1232 | } else if (existing.address && existing.chain) { |
| 1143 | | // Convert old single-address format |
| 1233 | // Convert old single-address format (add ID to migrated address) |
| 1144 | 1234 | addresses = [{ |
| 1235 | id: crypto.randomBytes(8).toString('hex'), |
| 1145 | 1236 | chain: existing.chain, |
| 1146 | 1237 | address: existing.address, |
| 1147 | | network: existing.network || network |
| 1238 | network: existing.network || network, |
| 1239 | label: '' |
| 1148 | 1240 | }] |
| 1149 | 1241 | } |
| 1150 | 1242 | |
| 1151 | | // Check if address already exists for this chain |
| 1152 | | const existingAddr = addresses.find(a => |
| 1243 | // Check if EXACT same address already exists (same address + same chain) |
| 1244 | const exactMatch = addresses.find(a => |
| 1153 | 1245 | a.chain === chain && a.address.toLowerCase() === address.toLowerCase() |
| 1154 | 1246 | ) |
| 1155 | | if (existingAddr) { |
| 1156 | | throw new Error(`Address already exists for ${chain}`) |
| 1247 | if (exactMatch) { |
| 1248 | throw new Error(`This exact address already exists for ${chain}`) |
| 1157 | 1249 | } |
| 1158 | 1250 | |
| 1159 | | // Add new address |
| 1160 | | addresses.push({ chain, address, network }) |
| 1251 | // Generate unique ID for this address |
| 1252 | const addressId = crypto.randomBytes(8).toString('hex') |
| 1253 | |
| 1254 | // Add new address with unique ID |
| 1255 | addresses.push({ |
| 1256 | id: addressId, |
| 1257 | chain, |
| 1258 | address, |
| 1259 | network, |
| 1260 | label: label || '', |
| 1261 | addedAt: Date.now() |
| 1262 | }) |
| 1161 | 1263 | |
| 1162 | 1264 | // Update contact with new addresses array |
| 1163 | 1265 | const updated = { |
| … | … | @@ -1184,12 +1286,12 @@ async function addAddressToContact(contactId, { chain, address, network }) { |
| 1184 | 1286 | } |
| 1185 | 1287 | |
| 1186 | 1288 | /** |
| 1187 | | * Remove an address from a contact |
| 1289 | * Remove an address from a contact by address ID |
| 1188 | 1290 | * @param {string} contactId - Contact ID |
| 1189 | | * @param {string} chain - Chain name to remove |
| 1291 | * @param {string} addressId - Address ID to remove |
| 1190 | 1292 | * @returns {Promise<Object>} - Updated contact |
| 1191 | 1293 | */ |
| 1192 | | async function removeAddressFromContact(contactId, chain) { |
| 1294 | async function removeAddressFromContact(contactId, addressId) { |
| 1193 | 1295 | if (!autobase) { |
| 1194 | 1296 | throw new Error('Storage not initialized') |
| 1195 | 1297 | } |
| … | … | @@ -1199,24 +1301,47 @@ async function removeAddressFromContact(contactId, chain) { |
| 1199 | 1301 | throw new Error('Contact not found') |
| 1200 | 1302 | } |
| 1201 | 1303 | |
| 1304 | // Migrate old format to new format if needed |
| 1305 | let addresses = [] |
| 1202 | 1306 | if (!existing.addresses || !Array.isArray(existing.addresses)) { |
| 1203 | | throw new Error('Contact has no addresses array') |
| 1307 | // Old format: single address |
| 1308 | if (existing.address && existing.chain) { |
| 1309 | // Get default network from config for migration |
| 1310 | const config = getConfig() |
| 1311 | const defaultNetwork = config.network || 'testnet' |
| 1312 | |
| 1313 | addresses = [{ |
| 1314 | id: crypto.randomBytes(8).toString('hex'), |
| 1315 | chain: existing.chain, |
| 1316 | address: existing.address, |
| 1317 | network: existing.network || defaultNetwork, |
| 1318 | label: '', |
| 1319 | addedAt: existing.addedAt || Date.now() |
| 1320 | }] |
| 1321 | } else { |
| 1322 | throw new Error('Contact has no addresses') |
| 1323 | } |
| 1324 | } else { |
| 1325 | addresses = existing.addresses |
| 1204 | 1326 | } |
| 1205 | 1327 | |
| 1206 | | // Filter out the address for this chain |
| 1207 | | const addresses = existing.addresses.filter(a => a.chain !== chain) |
| 1328 | // Filter out the address with this ID |
| 1329 | const newAddresses = addresses.filter(a => a.id !== addressId) |
| 1208 | 1330 | |
| 1209 | | if (addresses.length === existing.addresses.length) { |
| 1210 | | throw new Error(`No address found for chain: ${chain}`) |
| 1331 | if (newAddresses.length === addresses.length) { |
| 1332 | throw new Error(`No address found with ID: ${addressId}`) |
| 1211 | 1333 | } |
| 1212 | 1334 | |
| 1213 | | if (addresses.length === 0) { |
| 1335 | if (newAddresses.length === 0) { |
| 1214 | 1336 | throw new Error('Cannot remove last address. Delete contact instead.') |
| 1215 | 1337 | } |
| 1216 | 1338 | |
| 1217 | 1339 | const updated = { |
| 1218 | 1340 | ...existing, |
| 1219 | | addresses, |
| 1341 | addresses: newAddresses, |
| 1342 | // Remove old format fields |
| 1343 | address: undefined, |
| 1344 | chain: undefined, |
| 1220 | 1345 | modifiedAt: Date.now() |
| 1221 | 1346 | } |
| 1222 | 1347 | |
| … | … | @@ -1927,6 +2052,9 @@ module.exports = { |
| 1927 | 2052 | getAddresses, |
| 1928 | 2053 | getNextAddressIndex, |
| 1929 | 2054 | getCurrentAddress, |
| 2055 | updateAddressLabel, |
| 2056 | setCurrentAddress, |
| 2057 | archiveAddress, |
| 1930 | 2058 | // UTXO functions (Phase 6.3) |
| 1931 | 2059 | storeUTXO, |
| 1932 | 2060 | getUTXOs, |