| … | … | @@ -962,18 +962,25 @@ async function loadContactsList() { |
| 962 | 962 | html += '<div class="contact-addresses">' |
| 963 | 963 | for (const addr of addresses) { |
| 964 | 964 | const shortAddr = addr.address.substring(0, 16) + '...' |
| 965 | | const chainIcon = addr.chain === 'bitcoin' ? '₿' : addr.chain === 'ethereum' ? 'Ξ' : '🔗' |
| 965 | |
| 966 | // Get chain info from SUPPORTED_CHAINS |
| 967 | const chainInfo = SUPPORTED_CHAINS[addr.chain] || { icon: '🔗', name: addr.chain } |
| 968 | const chainIcon = chainInfo.icon |
| 969 | const chainName = chainInfo.name |
| 970 | |
| 971 | // Build display label: "Chain Name" or "Chain Name - Label" |
| 972 | const displayLabel = addr.label ? `${chainName} - ${addr.label}` : chainName |
| 966 | 973 | |
| 967 | 974 | html += ` |
| 968 | 975 | <div class="contact-address-row"> |
| 969 | 976 | <div class="contact-address-info"> |
| 970 | | <span class="chain-icon" title="${addr.chain}">${chainIcon}</span> |
| 977 | <span class="chain-icon" title="${displayLabel}">${chainIcon}</span> |
| 971 | 978 | <code class="contact-address copyable-address" data-address="${escapeHtml(addr.address)}" title="Click to copy full address">${shortAddr}</code> |
| 972 | | <span class="chain-badge chain-badge-${addr.chain}">${addr.chain}</span> |
| 979 | <span class="chain-badge chain-badge-${addr.chain}">${displayLabel}</span> |
| 973 | 980 | </div> |
| 974 | 981 | <div class="contact-address-actions"> |
| 975 | | <button class="btn-icon-small" onclick="sendToContactAddress('${contact.id}', '${addr.chain}')" title="Send ${addr.chain}">💸</button> |
| 976 | | <button class="btn-icon-small btn-danger" onclick="removeContactAddress('${contact.id}', '${addr.chain}', '${escapeHtml(contact.name)}')" title="Remove ${addr.chain} address">🗑️</button> |
| 982 | <button class="btn-icon-small" onclick="sendToContactAddress('${contact.id}', '${addr.id}')" title="Send to ${displayLabel}">💸</button> |
| 983 | <button class="btn-icon-small btn-danger" onclick="removeContactAddress('${contact.id}', '${addr.id}', '${escapeHtml(contact.name)}', '${escapeHtml(displayLabel)}')" title="Remove ${displayLabel} address">🗑️</button> |
| 977 | 984 | </div> |
| 978 | 985 | </div> |
| 979 | 986 | ` |
| … | … | @@ -1169,6 +1176,7 @@ async function submitAddWallet(event) { |
| 1169 | 1176 | const contactId = currentEditContactId |
| 1170 | 1177 | const chain = document.getElementById('add-wallet-chain').value |
| 1171 | 1178 | const address = document.getElementById('add-wallet-address').value.trim() |
| 1179 | const label = document.getElementById('add-wallet-label').value.trim() |
| 1172 | 1180 | |
| 1173 | 1181 | const errorDiv = document.getElementById('add-wallet-error') |
| 1174 | 1182 | errorDiv.style.display = 'none' |
| … | … | @@ -1177,7 +1185,7 @@ async function submitAddWallet(event) { |
| 1177 | 1185 | const res = await authFetch(`${API_BASE}/contacts/${contactId}/addresses`, { |
| 1178 | 1186 | method: 'POST', |
| 1179 | 1187 | headers: { 'Content-Type': 'application/json' }, |
| 1180 | | body: JSON.stringify({ chain, address, network: selectedNetwork }) |
| 1188 | body: JSON.stringify({ chain, address, network: selectedNetwork, label }) |
| 1181 | 1189 | }) |
| 1182 | 1190 | |
| 1183 | 1191 | const data = await res.json() |
| … | … | @@ -1188,7 +1196,8 @@ async function submitAddWallet(event) { |
| 1188 | 1196 | return |
| 1189 | 1197 | } |
| 1190 | 1198 | |
| 1191 | | showSuccess(`${chain.toUpperCase()} wallet added!`) |
| 1199 | const chainName = SUPPORTED_CHAINS[chain]?.name || chain.toUpperCase() |
| 1200 | showSuccess(`${chainName} wallet added!`) |
| 1192 | 1201 | closeAddWallet() |
| 1193 | 1202 | await loadContactsList() |
| 1194 | 1203 | } catch (err) { |
| … | … | @@ -1198,9 +1207,9 @@ async function submitAddWallet(event) { |
| 1198 | 1207 | } |
| 1199 | 1208 | |
| 1200 | 1209 | // Remove address from contact |
| 1201 | | async function removeContactAddress(contactId, chain, contactName) { |
| 1210 | async function removeContactAddress(contactId, addressId, contactName, chainLabel) { |
| 1202 | 1211 | const confirmed = confirm( |
| 1203 | | `Remove ${chain.toUpperCase()} wallet from "${contactName}"?\n\n` + |
| 1212 | `Remove ${chainLabel} wallet from "${contactName}"?\n\n` + |
| 1204 | 1213 | `This will remove this wallet address from the contact.\n\n` + |
| 1205 | 1214 | `Continue?` |
| 1206 | 1215 | ) |
| … | … | @@ -1208,7 +1217,7 @@ async function removeContactAddress(contactId, chain, contactName) { |
| 1208 | 1217 | if (!confirmed) return |
| 1209 | 1218 | |
| 1210 | 1219 | try { |
| 1211 | | const res = await authFetch(`${API_BASE}/contacts/${contactId}/addresses/${chain}`, { |
| 1220 | const res = await authFetch(`${API_BASE}/contacts/${contactId}/addresses/${addressId}`, { |
| 1212 | 1221 | method: 'DELETE' |
| 1213 | 1222 | }) |
| 1214 | 1223 | |
| … | … | @@ -1219,7 +1228,7 @@ async function removeContactAddress(contactId, chain, contactName) { |
| 1219 | 1228 | return |
| 1220 | 1229 | } |
| 1221 | 1230 | |
| 1222 | | showSuccess(`${chain.toUpperCase()} wallet removed`) |
| 1231 | showSuccess(`${chainLabel} wallet removed`) |
| 1223 | 1232 | await loadContactsList() |
| 1224 | 1233 | } catch (err) { |
| 1225 | 1234 | showError(`Error: ${err.message}`) |
| … | … | @@ -1227,29 +1236,35 @@ async function removeContactAddress(contactId, chain, contactName) { |
| 1227 | 1236 | } |
| 1228 | 1237 | |
| 1229 | 1238 | // Send to specific address of a contact |
| 1230 | | async function sendToContactAddress(contactId, chain) { |
| 1239 | async function sendToContactAddress(contactId, addressId) { |
| 1231 | 1240 | const contact = currentContacts.find(c => c.id === contactId) |
| 1232 | 1241 | if (!contact) { |
| 1233 | 1242 | showError('Contact not found') |
| 1234 | 1243 | return |
| 1235 | 1244 | } |
| 1236 | 1245 | |
| 1237 | | // Find the address for this chain |
| 1246 | // Find the specific address by ID |
| 1247 | let addrInfo = null |
| 1238 | 1248 | let address = null |
| 1249 | let chain = null |
| 1239 | 1250 | |
| 1240 | 1251 | if (contact.addresses && Array.isArray(contact.addresses)) { |
| 1241 | | // NEW FORMAT: Multiple addresses |
| 1242 | | const addrInfo = contact.addresses.find(a => a.chain === chain) |
| 1252 | // NEW FORMAT: Multiple addresses with IDs |
| 1253 | addrInfo = contact.addresses.find(a => a.id === addressId) |
| 1243 | 1254 | if (addrInfo) { |
| 1244 | 1255 | address = addrInfo.address |
| 1256 | chain = addrInfo.chain |
| 1245 | 1257 | } |
| 1246 | | } else if (contact.chain === chain && contact.address) { |
| 1247 | | // OLD FORMAT: Single address |
| 1258 | } |
| 1259 | |
| 1260 | // Fallback for old format (single address, no ID) |
| 1261 | if (!address && contact.address && contact.chain) { |
| 1248 | 1262 | address = contact.address |
| 1263 | chain = contact.chain |
| 1249 | 1264 | } |
| 1250 | 1265 | |
| 1251 | | if (!address) { |
| 1252 | | showError(`No ${chain} address for ${contact.name}`) |
| 1266 | if (!address || !chain) { |
| 1267 | showError(`Address not found for ${contact.name}`) |
| 1253 | 1268 | return |
| 1254 | 1269 | } |
| 1255 | 1270 | |
| … | … | @@ -1414,6 +1429,7 @@ async function resetWallet() { |
| 1414 | 1429 | } |
| 1415 | 1430 | |
| 1416 | 1431 | // Supported chains configuration (extensible for future chains) |
| 1432 | // Only chains with registered WDK wallets are included |
| 1417 | 1433 | const SUPPORTED_CHAINS = { |
| 1418 | 1434 | bitcoin: { |
| 1419 | 1435 | name: 'Bitcoin', |
| … | … | @@ -1453,10 +1469,18 @@ const SUPPORTED_CHAINS = { |
| 1453 | 1469 | }, |
| 1454 | 1470 | feeReserve: 100000 |
| 1455 | 1471 | } |
| 1456 | | // Future chains can be added here: |
| 1457 | | // solana: { ... }, |
| 1458 | | // cardano: { ... }, |
| 1459 | | // etc. |
| 1472 | // TODO: Add more chains when WDK support is available |
| 1473 | // These EVM chains can be added by registering WalletManagerEvm with different RPC endpoints: |
| 1474 | // - polygon (Polygon/MATIC) |
| 1475 | // - bsc (BNB Chain) |
| 1476 | // - arbitrum (Arbitrum One) |
| 1477 | // - optimism (Optimism) |
| 1478 | // - avalanche (Avalanche C-Chain) |
| 1479 | // |
| 1480 | // Future non-EVM chains require new WDK wallet implementations: |
| 1481 | // - solana (wdk-wallet-solana) |
| 1482 | // - cardano (wdk-wallet-cardano) |
| 1483 | // - etc. |
| 1460 | 1484 | } |
| 1461 | 1485 | |
| 1462 | 1486 | // Currency state management |
| … | … | @@ -1521,6 +1545,78 @@ function updateWalletView() { |
| 1521 | 1545 | document.getElementById('wallet-name').textContent = chain.name |
| 1522 | 1546 | document.getElementById('wallet-network').textContent = networkName |
| 1523 | 1547 | } |
| 1548 | |
| 1549 | // Load wallet addresses for selector |
| 1550 | loadWalletAddressSelector() |
| 1551 | } |
| 1552 | |
| 1553 | // Load wallet addresses into the selector dropdown |
| 1554 | async function loadWalletAddressSelector() { |
| 1555 | const select = document.getElementById('wallet-address-select') |
| 1556 | select.innerHTML = '<option value="">Loading...</option>' |
| 1557 | |
| 1558 | try { |
| 1559 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${selectedCurrency}`) |
| 1560 | const data = await res.json() |
| 1561 | |
| 1562 | if (data.error) { |
| 1563 | select.innerHTML = '<option value="">Error loading addresses</option>' |
| 1564 | return |
| 1565 | } |
| 1566 | |
| 1567 | if (data.addresses.length === 0) { |
| 1568 | select.innerHTML = '<option value="">No addresses yet</option>' |
| 1569 | return |
| 1570 | } |
| 1571 | |
| 1572 | // Build options |
| 1573 | let html = '' |
| 1574 | for (const addr of data.addresses) { |
| 1575 | const shortAddr = addr.address.substring(0, 12) + '...' |
| 1576 | const label = addr.label || `Address ${addr.index}` |
| 1577 | const displayText = `${label} - ${shortAddr}` |
| 1578 | const currentMarker = addr.isCurrent ? ' ⭐' : '' |
| 1579 | |
| 1580 | html += `<option value="${addr.index}" ${addr.isCurrent ? 'selected' : ''}>${displayText}${currentMarker}</option>` |
| 1581 | } |
| 1582 | |
| 1583 | select.innerHTML = html |
| 1584 | } catch (err) { |
| 1585 | console.error('Load address selector error:', err) |
| 1586 | select.innerHTML = '<option value="">Failed to load</option>' |
| 1587 | } |
| 1588 | } |
| 1589 | |
| 1590 | // Handle wallet address selector change |
| 1591 | async function onWalletAddressChange() { |
| 1592 | const select = document.getElementById('wallet-address-select') |
| 1593 | const selectedIndex = parseInt(select.value) |
| 1594 | |
| 1595 | if (isNaN(selectedIndex)) return |
| 1596 | |
| 1597 | try { |
| 1598 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${selectedCurrency}/${selectedIndex}/current`, { |
| 1599 | method: 'POST' |
| 1600 | }) |
| 1601 | |
| 1602 | const data = await res.json() |
| 1603 | |
| 1604 | if (data.error) { |
| 1605 | showError(data.error) |
| 1606 | return |
| 1607 | } |
| 1608 | |
| 1609 | showSuccess('Wallet address changed!') |
| 1610 | |
| 1611 | // Reload balance and QR code |
| 1612 | await loadCurrentBalance() |
| 1613 | await loadCurrentQRCode() |
| 1614 | |
| 1615 | // Reload the selector to update the star marker |
| 1616 | await loadWalletAddressSelector() |
| 1617 | } catch (err) { |
| 1618 | showError(`Failed to change address: ${err.message}`) |
| 1619 | } |
| 1524 | 1620 | } |
| 1525 | 1621 | |
| 1526 | 1622 | // Load balance for current currency |
| … | … | @@ -1698,6 +1794,16 @@ function showError(message) { |
| 1698 | 1794 | const toast = document.getElementById('error-toast') |
| 1699 | 1795 | toast.textContent = message |
| 1700 | 1796 | toast.classList.add('show') |
| 1797 | |
| 1798 | // If a modal is open, append toast to it (so it appears above backdrop) |
| 1799 | const openDialog = document.querySelector('dialog[open]') |
| 1800 | if (openDialog && !openDialog.contains(toast)) { |
| 1801 | openDialog.appendChild(toast) |
| 1802 | } else if (!openDialog && toast.parentElement !== document.body) { |
| 1803 | // If no dialog open, move back to body |
| 1804 | document.body.appendChild(toast) |
| 1805 | } |
| 1806 | |
| 1701 | 1807 | setTimeout(() => { |
| 1702 | 1808 | toast.classList.remove('show') |
| 1703 | 1809 | }, 5000) |
| … | … | @@ -1802,7 +1908,16 @@ async function loadTransactions() { |
| 1802 | 1908 | const contacts = contactsData.contacts || [] |
| 1803 | 1909 | const addressToContact = {} |
| 1804 | 1910 | for (const contact of contacts) { |
| 1805 | | addressToContact[contact.address.toLowerCase()] = contact |
| 1911 | // Handle both old format (single address) and new format (addresses array) |
| 1912 | if (contact.addresses && Array.isArray(contact.addresses)) { |
| 1913 | // New format: multiple addresses |
| 1914 | for (const addr of contact.addresses) { |
| 1915 | addressToContact[addr.address.toLowerCase()] = contact |
| 1916 | } |
| 1917 | } else if (contact.address) { |
| 1918 | // Old format: single address |
| 1919 | addressToContact[contact.address.toLowerCase()] = contact |
| 1920 | } |
| 1806 | 1921 | } |
| 1807 | 1922 | |
| 1808 | 1923 | // Filter by selected network AND selected currency |
| … | … | @@ -1889,8 +2004,129 @@ async function refreshAll() { |
| 1889 | 2004 | btn.textContent = '🔄 Refresh' |
| 1890 | 2005 | } |
| 1891 | 2006 | |
| 2007 | // Current chain for receive modal |
| 2008 | let receiveModalChain = null |
| 2009 | let manageAddressesChain = null |
| 2010 | |
| 2011 | // Show manage addresses modal |
| 2012 | async function showManageAddresses() { |
| 2013 | manageAddressesChain = selectedCurrency |
| 2014 | |
| 2015 | const modal = document.getElementById('manage-addresses-modal') |
| 2016 | const chainInfo = SUPPORTED_CHAINS[selectedCurrency] || { icon: '🔗', name: selectedCurrency } |
| 2017 | const chainName = chainInfo.name |
| 2018 | const networkLabel = selectedNetwork === 'testnet' |
| 2019 | ? (selectedCurrency === 'ethereum' ? 'Sepolia' : 'Testnet') |
| 2020 | : 'Mainnet' |
| 2021 | |
| 2022 | document.getElementById('manage-addresses-chain').textContent = `- ${chainName} (${networkLabel})` |
| 2023 | |
| 2024 | // Load addresses |
| 2025 | await loadManageAddresses(selectedCurrency) |
| 2026 | |
| 2027 | modal.showModal() |
| 2028 | } |
| 2029 | |
| 2030 | // Close manage addresses modal |
| 2031 | function closeManageAddresses() { |
| 2032 | document.getElementById('manage-addresses-modal').close() |
| 2033 | manageAddressesChain = null |
| 2034 | } |
| 2035 | |
| 2036 | // Load addresses for manage modal |
| 2037 | async function loadManageAddresses(chain) { |
| 2038 | const container = document.getElementById('manage-addresses-list') |
| 2039 | container.innerHTML = '<p class="loading-text">Loading addresses...</p>' |
| 2040 | |
| 2041 | try { |
| 2042 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${chain}`) |
| 2043 | const data = await res.json() |
| 2044 | |
| 2045 | if (data.error) { |
| 2046 | container.innerHTML = `<p class="error-text">${data.error}</p>` |
| 2047 | return |
| 2048 | } |
| 2049 | |
| 2050 | if (data.addresses.length === 0) { |
| 2051 | container.innerHTML = '<p class="info-text">No addresses yet. Click "Derive New Address" to create your first address!</p>' |
| 2052 | return |
| 2053 | } |
| 2054 | |
| 2055 | // Build address list HTML |
| 2056 | let html = '' |
| 2057 | for (const addr of data.addresses) { |
| 2058 | const isCurrent = addr.isCurrent |
| 2059 | const currentBadge = isCurrent ? '<span class="badge badge-primary">Current</span>' : '' |
| 2060 | const labelDisplay = addr.label || `Address ${addr.index}` |
| 2061 | |
| 2062 | html += ` |
| 2063 | <div class="wallet-address-item ${isCurrent ? 'current' : ''}"> |
| 2064 | <div class="address-info-full"> |
| 2065 | <div class="address-label-row"> |
| 2066 | <input type="text" |
| 2067 | class="address-label-input ${addr.label ? '' : 'placeholder'}" |
| 2068 | value="${addr.label || `Address ${addr.index}`}" |
| 2069 | onblur="updateWalletAddressLabel('${chain}', ${addr.index}, this.value)" |
| 2070 | placeholder="Add label (e.g., Hardware wallet, Hot wallet)..." |
| 2071 | /> |
| 2072 | ${currentBadge} |
| 2073 | </div> |
| 2074 | <div class="address-full-row"> |
| 2075 | <code class="address-code-full" title="Click to copy" onclick="copyWalletAddress('${addr.address}')">${addr.address}</code> |
| 2076 | </div> |
| 2077 | <div class="address-meta"> |
| 2078 | <span class="meta-item">Index: ${addr.index}</span> |
| 2079 | <span class="meta-item">Path: ${addr.path}</span> |
| 2080 | <span class="meta-item">Created: ${new Date(addr.createdAt).toLocaleDateString()}</span> |
| 2081 | </div> |
| 2082 | </div> |
| 2083 | <div class="address-actions-vertical"> |
| 2084 | ${!isCurrent ? `<button class="btn-secondary btn-sm" onclick="setWalletAddressCurrent('${chain}', ${addr.index})">Set as Current</button>` : ''} |
| 2085 | <button class="btn-secondary btn-sm" onclick="copyWalletAddress('${addr.address}')">Copy Address</button> |
| 2086 | ${!isCurrent ? `<button class="btn-danger btn-sm" onclick="archiveWalletAddress('${chain}', ${addr.index})">Archive</button>` : ''} |
| 2087 | </div> |
| 2088 | </div> |
| 2089 | ` |
| 2090 | } |
| 2091 | |
| 2092 | container.innerHTML = html |
| 2093 | } catch (err) { |
| 2094 | console.error('Load addresses error:', err) |
| 2095 | container.innerHTML = `<p class="error-text">Failed to load addresses</p>` |
| 2096 | } |
| 2097 | } |
| 2098 | |
| 2099 | // Derive new address from manage modal |
| 2100 | async function deriveNewAddressFromManage() { |
| 2101 | if (!manageAddressesChain) return |
| 2102 | |
| 2103 | const label = prompt('Enter a label for this address (optional):\n\nExamples: "Hardware wallet", "Hot wallet", "MetaMask", "Savings"', '') |
| 2104 | if (label === null) return // User cancelled |
| 2105 | |
| 2106 | try { |
| 2107 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${manageAddressesChain}/derive`, { |
| 2108 | method: 'POST', |
| 2109 | body: JSON.stringify({ label: label || '' }) |
| 2110 | }) |
| 2111 | |
| 2112 | const data = await res.json() |
| 2113 | |
| 2114 | if (data.error) { |
| 2115 | showError(data.error) |
| 2116 | return |
| 2117 | } |
| 2118 | |
| 2119 | showSuccess('New address derived!') |
| 2120 | await loadManageAddresses(manageAddressesChain) |
| 2121 | } catch (err) { |
| 2122 | showError(`Failed to derive address: ${err.message}`) |
| 2123 | } |
| 2124 | } |
| 2125 | |
| 1892 | 2126 | // Show receive modal |
| 1893 | 2127 | async function showReceive(chain) { |
| 2128 | receiveModalChain = chain |
| 2129 | |
| 1894 | 2130 | const modal = document.getElementById('receive-modal') |
| 1895 | 2131 | const addressInput = document.getElementById('receive-address') |
| 1896 | 2132 | const chainName = chain.charAt(0).toUpperCase() + chain.slice(1) |
| … | … | @@ -1900,8 +2136,9 @@ async function showReceive(chain) { |
| 1900 | 2136 | |
| 1901 | 2137 | document.getElementById('receive-chain').textContent = `${chainName} (${networkLabel})` |
| 1902 | 2138 | |
| 1903 | | // Load address |
| 2139 | // Load current address |
| 1904 | 2140 | addressInput.value = 'Loading...' |
| 2141 | document.getElementById('receive-address-label').textContent = '' |
| 1905 | 2142 | |
| 1906 | 2143 | try { |
| 1907 | 2144 | const res = await authFetch(`${API_BASE}/address/${selectedNetwork}-${chain}`) |
| … | … | @@ -1914,11 +2151,21 @@ async function showReceive(chain) { |
| 1914 | 2151 | |
| 1915 | 2152 | addressInput.value = data.address |
| 1916 | 2153 | |
| 2154 | // Show label if present |
| 2155 | if (data.label) { |
| 2156 | document.getElementById('receive-address-label').textContent = `Label: ${data.label}` |
| 2157 | } else { |
| 2158 | document.getElementById('receive-address-label').textContent = 'Current receiving address' |
| 2159 | } |
| 2160 | |
| 1917 | 2161 | // Display address in large format (no QR code - not available in holepunchto) |
| 1918 | 2162 | document.getElementById('qr-code').innerHTML = ` |
| 1919 | 2163 | <div class="address-large">${data.address}</div> |
| 1920 | 2164 | ` |
| 1921 | 2165 | |
| 2166 | // Load wallet addresses list |
| 2167 | await loadWalletAddresses(chain) |
| 2168 | |
| 1922 | 2169 | } catch (err) { |
| 1923 | 2170 | console.error('Address error:', err) |
| 1924 | 2171 | addressInput.value = 'Failed to load address' |
| … | … | @@ -1927,6 +2174,182 @@ async function showReceive(chain) { |
| 1927 | 2174 | modal.showModal() |
| 1928 | 2175 | } |
| 1929 | 2176 | |
| 2177 | // Load wallet addresses for chain |
| 2178 | async function loadWalletAddresses(chain) { |
| 2179 | const container = document.getElementById('wallet-addresses-list') |
| 2180 | container.innerHTML = '<p class="loading-text">Loading addresses...</p>' |
| 2181 | |
| 2182 | try { |
| 2183 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${chain}`) |
| 2184 | const data = await res.json() |
| 2185 | |
| 2186 | if (data.error) { |
| 2187 | container.innerHTML = `<p class="error-text">${data.error}</p>` |
| 2188 | return |
| 2189 | } |
| 2190 | |
| 2191 | if (data.addresses.length === 0) { |
| 2192 | container.innerHTML = '<p class="info-text">No addresses yet. Derive one to get started!</p>' |
| 2193 | return |
| 2194 | } |
| 2195 | |
| 2196 | // Build address list HTML |
| 2197 | let html = '' |
| 2198 | for (const addr of data.addresses) { |
| 2199 | const shortAddr = addr.address.substring(0, 20) + '...' |
| 2200 | const isCurrent = addr.isCurrent |
| 2201 | const currentBadge = isCurrent ? '<span class="badge badge-primary">Current</span>' : '' |
| 2202 | const labelDisplay = addr.label || `Address ${addr.index}` |
| 2203 | |
| 2204 | html += ` |
| 2205 | <div class="wallet-address-item ${isCurrent ? 'current' : ''}"> |
| 2206 | <div class="address-info"> |
| 2207 | <div class="address-label-row"> |
| 2208 | <input type="text" |
| 2209 | class="address-label-input ${addr.label ? '' : 'placeholder'}" |
| 2210 | value="${addr.label || `Address ${addr.index}`}" |
| 2211 | onblur="updateWalletAddressLabel('${chain}', ${addr.index}, this.value)" |
| 2212 | placeholder="Add label..." |
| 2213 | /> |
| 2214 | ${currentBadge} |
| 2215 | </div> |
| 2216 | <code class="address-code" title="${addr.address}">${shortAddr}</code> |
| 2217 | </div> |
| 2218 | <div class="address-actions"> |
| 2219 | ${!isCurrent ? `<button class="btn-icon-small" onclick="setWalletAddressCurrent('${chain}', ${addr.index})" title="Set as current">⭐</button>` : ''} |
| 2220 | <button class="btn-icon-small" onclick="copyWalletAddress('${addr.address}')" title="Copy">📋</button> |
| 2221 | ${!isCurrent ? `<button class="btn-icon-small btn-danger" onclick="archiveWalletAddress('${chain}', ${addr.index})" title="Archive">🗑️</button>` : ''} |
| 2222 | </div> |
| 2223 | </div> |
| 2224 | ` |
| 2225 | } |
| 2226 | |
| 2227 | container.innerHTML = html |
| 2228 | } catch (err) { |
| 2229 | console.error('Load addresses error:', err) |
| 2230 | container.innerHTML = `<p class="error-text">Failed to load addresses</p>` |
| 2231 | } |
| 2232 | } |
| 2233 | |
| 2234 | // Derive new address |
| 2235 | async function deriveNewAddress() { |
| 2236 | if (!receiveModalChain) return |
| 2237 | |
| 2238 | const label = prompt('Enter a label for this address (optional):', '') |
| 2239 | |
| 2240 | try { |
| 2241 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${receiveModalChain}/derive`, { |
| 2242 | method: 'POST', |
| 2243 | body: JSON.stringify({ label: label || '' }) |
| 2244 | }) |
| 2245 | |
| 2246 | const data = await res.json() |
| 2247 | |
| 2248 | if (data.error) { |
| 2249 | showError(data.error) |
| 2250 | return |
| 2251 | } |
| 2252 | |
| 2253 | showSuccess('New address derived!') |
| 2254 | await loadWalletAddresses(receiveModalChain) |
| 2255 | } catch (err) { |
| 2256 | showError(`Failed to derive address: ${err.message}`) |
| 2257 | } |
| 2258 | } |
| 2259 | |
| 2260 | // Update wallet address label |
| 2261 | async function updateWalletAddressLabel(chain, index, label) { |
| 2262 | try { |
| 2263 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${chain}/${index}`, { |
| 2264 | method: 'PUT', |
| 2265 | body: JSON.stringify({ label }) |
| 2266 | }) |
| 2267 | |
| 2268 | const data = await res.json() |
| 2269 | |
| 2270 | if (data.error) { |
| 2271 | showError(data.error) |
| 2272 | return |
| 2273 | } |
| 2274 | |
| 2275 | showSuccess('Label updated!') |
| 2276 | } catch (err) { |
| 2277 | console.error('Update label error:', err) |
| 2278 | } |
| 2279 | } |
| 2280 | |
| 2281 | // Set wallet address as current |
| 2282 | async function setWalletAddressCurrent(chain, index) { |
| 2283 | try { |
| 2284 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${chain}/${index}/current`, { |
| 2285 | method: 'POST' |
| 2286 | }) |
| 2287 | |
| 2288 | const data = await res.json() |
| 2289 | |
| 2290 | if (data.error) { |
| 2291 | showError(data.error) |
| 2292 | return |
| 2293 | } |
| 2294 | |
| 2295 | showSuccess('Address set as current!') |
| 2296 | |
| 2297 | // Reload both modals if open |
| 2298 | if (receiveModalChain) { |
| 2299 | await showReceive(chain) |
| 2300 | } |
| 2301 | if (manageAddressesChain) { |
| 2302 | await loadManageAddresses(chain) |
| 2303 | } |
| 2304 | |
| 2305 | // Update main wallet view |
| 2306 | await updateWalletView() |
| 2307 | } catch (err) { |
| 2308 | showError(`Failed to set current: ${err.message}`) |
| 2309 | } |
| 2310 | } |
| 2311 | |
| 2312 | // Copy wallet address to clipboard |
| 2313 | async function copyWalletAddress(address) { |
| 2314 | try { |
| 2315 | await navigator.clipboard.writeText(address) |
| 2316 | showSuccess('Address copied!') |
| 2317 | } catch (err) { |
| 2318 | showError('Failed to copy address') |
| 2319 | } |
| 2320 | } |
| 2321 | |
| 2322 | // Archive wallet address |
| 2323 | async function archiveWalletAddress(chain, index) { |
| 2324 | const confirmed = confirm('Archive this address? It will be hidden from the list.') |
| 2325 | if (!confirmed) return |
| 2326 | |
| 2327 | try { |
| 2328 | const res = await authFetch(`${API_BASE}/addresses/${selectedNetwork}-${chain}/${index}`, { |
| 2329 | method: 'DELETE' |
| 2330 | }) |
| 2331 | |
| 2332 | const data = await res.json() |
| 2333 | |
| 2334 | if (data.error) { |
| 2335 | showError(data.error) |
| 2336 | return |
| 2337 | } |
| 2338 | |
| 2339 | showSuccess('Address archived!') |
| 2340 | |
| 2341 | // Reload both modals if open |
| 2342 | if (receiveModalChain) { |
| 2343 | await loadWalletAddresses(chain) |
| 2344 | } |
| 2345 | if (manageAddressesChain) { |
| 2346 | await loadManageAddresses(chain) |
| 2347 | } |
| 2348 | } catch (err) { |
| 2349 | showError(`Failed to archive: ${err.message}`) |
| 2350 | } |
| 2351 | } |
| 2352 | |
| 1930 | 2353 | // Close receive modal |
| 1931 | 2354 | function closeReceive() { |
| 1932 | 2355 | document.getElementById('receive-modal').close() |
| … | … | @@ -1952,6 +2375,16 @@ function showSuccess(message) { |
| 1952 | 2375 | const toast = document.getElementById('error-toast') |
| 1953 | 2376 | toast.textContent = message |
| 1954 | 2377 | toast.classList.add('show', 'success') |
| 2378 | |
| 2379 | // If a modal is open, append toast to it (so it appears above backdrop) |
| 2380 | const openDialog = document.querySelector('dialog[open]') |
| 2381 | if (openDialog && !openDialog.contains(toast)) { |
| 2382 | openDialog.appendChild(toast) |
| 2383 | } else if (!openDialog && toast.parentElement !== document.body) { |
| 2384 | // If no dialog open, move back to body |
| 2385 | document.body.appendChild(toast) |
| 2386 | } |
| 2387 | |
| 1955 | 2388 | setTimeout(() => { |
| 1956 | 2389 | toast.classList.remove('show', 'success') |
| 1957 | 2390 | }, 3000) |