OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet commits / 42ce0ad

Fix contact list: multi-address support + send modal fixes
Deployment: testing (hyper-wallet-test) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
42ce0adf015389d6f7643a22d8ec108a63f5ae3e
by plankatron <fleeky@gmail.com> · 2026-01-27T21:38:14.000Z

Changed files (2) +262 -64

ui/app.js +134 -64
……@@ -914,55 +914,74 @@ async function loadContactsList() {
914914 return
915915 }
916916
917 // Group contacts by chain
918 const byChain = {}
919 for (const contact of currentContacts) {
920 if (!byChain[contact.chain]) byChain[contact.chain] = []
921 byChain[contact.chain].push(contact)
922 }
917 // Sort contacts alphabetically
918 currentContacts.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
923919
924 let html = ''
920 let html = '<div class="contacts-grid">'
921
922 for (const contact of currentContacts) {
923 const favIcon = contact.metadata?.favorite ? '⭐' : ''
924
925 // Support both old format (single address) and new format (multiple addresses)
926 let addresses = []
927 if (contact.addresses && Array.isArray(contact.addresses)) {
928 // NEW FORMAT: Multiple addresses
929 addresses = contact.addresses
930 } else if (contact.chain && contact.address) {
931 // OLD FORMAT: Single address - convert to array
932 addresses = [{
933 chain: contact.chain,
934 address: contact.address,
935 network: contact.network || selectedNetwork
936 }]
937 }
925938
926 for (const [chain, contacts] of Object.entries(byChain)) {
927939 html += `
928 <div class="contacts-chain-section">
929 <h4 class="chain-header">${chain.toUpperCase()}</h4>
930 <div class="contacts-grid">
940 <div class="contact-card">
941 <div class="contact-header">
942 <span class="contact-name">${favIcon} ${escapeHtml(contact.name)}</span>
943 <div class="contact-actions-inline">
944 <button class="btn-icon-small" onclick="editContact('${contact.id}')" title="Edit">✏️</button>
945 <button class="btn-icon-small" onclick="deleteContactConfirm('${contact.id}', '${escapeHtml(contact.name)}')" title="Delete">🗑️</button>
946 </div>
947 </div>
931948 `
932949
933 for (const contact of contacts) {
934 const shortAddr = contact.address.substring(0, 16) + '...'
935 const favIcon = contact.metadata?.favorite ? '⭐' : ''
936
937 html += `
938 <div class="contact-card">
939 <div class="contact-header">
940 <span class="contact-name">${favIcon} ${escapeHtml(contact.name)}</span>
941 <div class="contact-actions-inline">
942 <button class="btn-icon-small" onclick="editContact('${contact.id}')" title="Edit">✏️</button>
943 <button class="btn-icon-small" onclick="deleteContactConfirm('${contact.id}', '${escapeHtml(contact.name)}')" title="Delete">🗑️</button>
950 // Show all addresses
951 if (addresses.length > 0) {
952 html += '<div class="contact-addresses">'
953 for (const addr of addresses) {
954 const shortAddr = addr.address.substring(0, 16) + '...'
955 const chainIcon = addr.chain === 'bitcoin' ? '₿' : addr.chain === 'ethereum' ? 'Ξ' : '🔗'
956
957 html += `
958 <div class="contact-address-row">
959 <div class="contact-address-info">
960 <span class="chain-icon" title="${addr.chain}">${chainIcon}</span>
961 <code class="contact-address" onclick="copyToClipboard('${addr.address}', 'Address copied!')" title="Click to copy">${shortAddr}</code>
962 <span class="chain-badge chain-badge-${addr.chain}">${addr.chain}</span>
944963 </div>
964 <button class="btn-icon-small" onclick="sendToContactAddress('${contact.id}', '${addr.chain}')" title="Send ${addr.chain}">💸</button>
945965 </div>
946 <div class="contact-details">
947 <code class="contact-address" onclick="copyToClipboard('${contact.address}', 'Address copied!')" title="Click to copy">${shortAddr}</code>
948 <span class="chain-badge chain-badge-${chain}">${chain}</span>
949 </div>
950 ${contact.notes ? `<p class="contact-notes">${escapeHtml(contact.notes)}</p>` : ''}
951 <div class="contact-footer">
952 <button class="btn-secondary btn-small btn-full-width" onclick="sendToContact('${contact.id}')">💸 Send to ${escapeHtml(contact.name)}</button>
953 </div>
954 </div>
955 `
966 `
967 }
968 html += '</div>'
969 } else {
970 html += '<p class="contact-error">⚠️ No addresses configured</p>'
956971 }
957972
958 html += `
959 </div>
960 </div>
961 `
973 if (contact.notes) {
974 html += `<p class="contact-notes">${escapeHtml(contact.notes)}</p>`
975 }
976
977 html += '</div>'
962978 }
963979
980 html += '</div>'
981
964982 contactsList.innerHTML = html
965983 } catch (err) {
984 console.error('Load contacts error:', err)
966985 contactsList.innerHTML = `<div class="error-message">Error loading contacts: ${err.message}</div>`
967986 }
968987}
……@@ -1104,47 +1123,79 @@ async function deleteContactConfirm(contactId, contactName) {
11041123 }
11051124}
11061125
1107async function sendToContact(contactId) {
1126// Send to specific address of a contact
1127async function sendToContactAddress(contactId, chain) {
11081128 const contact = currentContacts.find(c => c.id === contactId)
11091129 if (!contact) {
11101130 showError('Contact not found')
11111131 return
11121132 }
11131133
1114 // Close contacts modal
1115 closeContacts()
1134 // Find the address for this chain
1135 let address = null
11161136
1117 // Set currency to match contact's chain
1118 const chainMap = {
1119 'bitcoin': 'testnet-bitcoin',
1120 'ethereum': 'testnet-ethereum'
1137 if (contact.addresses && Array.isArray(contact.addresses)) {
1138 // NEW FORMAT: Multiple addresses
1139 const addrInfo = contact.addresses.find(a => a.chain === chain)
1140 if (addrInfo) {
1141 address = addrInfo.address
1142 }
1143 } else if (contact.chain === chain && contact.address) {
1144 // OLD FORMAT: Single address
1145 address = contact.address
11211146 }
11221147
1123 const currencyKey = chainMap[contact.chain]
1124 if (currencyKey) {
1125 document.getElementById('currency-selector').value = currencyKey
1126 switchCurrency()
1148 if (!address) {
1149 showError(`No ${chain} address for ${contact.name}`)
1150 return
11271151 }
11281152
1153 // Close contacts modal
1154 closeContacts()
1155
1156 // Set currency to match chain
1157 selectedCurrency = chain
1158 localStorage.setItem('currency', chain)
1159 document.getElementById('currency-selector').value = chain
1160
1161 // Update wallet view
1162 updateWalletView()
1163
11291164 // Show send form
1130 await showSendForCurrent()
1165 await showSend(chain)
11311166
11321167 // Pre-fill address
11331168 setTimeout(() => {
1134 const addressInput = document.getElementById('send-address')
1169 const addressInput = document.getElementById('send-to')
11351170 if (addressInput) {
1136 addressInput.value = contact.address
1137
1138 // Also update the recipient display if there is one
1139 const recipientDisplay = document.getElementById('send-recipient-name')
1140 if (recipientDisplay) {
1141 recipientDisplay.textContent = `Sending to: ${contact.name}`
1142 recipientDisplay.style.display = 'block'
1143 }
1171 addressInput.value = address
11441172 }
11451173 }, 100)
11461174}
11471175
1176// Legacy function - send to first available address
1177async function sendToContact(contactId) {
1178 const contact = currentContacts.find(c => c.id === contactId)
1179 if (!contact) {
1180 showError('Contact not found')
1181 return
1182 }
1183
1184 // Get first available address
1185 let chain = null
1186 if (contact.addresses && Array.isArray(contact.addresses) && contact.addresses.length > 0) {
1187 chain = contact.addresses[0].chain
1188 } else if (contact.chain) {
1189 chain = contact.chain
1190 } else {
1191 showError('Contact has no addresses')
1192 return
1193 }
1194
1195 // Use the chain-specific function
1196 await sendToContactAddress(contactId, chain)
1197}
1198
11481199function escapeHtml(text) {
11491200 const div = document.createElement('div')
11501201 div.textContent = text
……@@ -1928,14 +1979,33 @@ function closeSend() {
19281979
19291980// Set amount to maximum balance
19301981function setMaxAmount() {
1931 if (currentBalance > 0) {
1932 // Reserve some for fees (from chain config)
1933 const chain = SUPPORTED_CHAINS[currentSendChain]
1934 const reserved = chain ? chain.feeReserve : 0
1935 const maxAmount = Math.max(0, currentBalance - reserved)
1936 document.getElementById('send-amount').value = maxAmount
1937 estimateFee()
1982 console.log('setMaxAmount called', { currentBalance, currentSendChain })
1983
1984 if (!currentSendChain) {
1985 showError('Chain not selected')
1986 return
1987 }
1988
1989 if (!currentBalance || currentBalance <= 0) {
1990 showError('No balance available')
1991 return
1992 }
1993
1994 // Reserve some for fees (from chain config)
1995 const chain = SUPPORTED_CHAINS[currentSendChain]
1996 if (!chain) {
1997 console.error('Chain config not found:', currentSendChain)
1998 showError('Invalid chain configuration')
1999 return
19382000 }
2001
2002 const reserved = chain.feeReserve || 0
2003 const maxAmount = Math.max(0, currentBalance - reserved)
2004
2005 console.log('Setting max amount:', { maxAmount, reserved, currentBalance })
2006
2007 document.getElementById('send-amount').value = maxAmount
2008 estimateFee()
19392009}
19402010
19412011// Estimate transaction fee
ui/style.css +128 -0
……@@ -1624,3 +1624,131 @@ footer a:hover {
16241624 background: var(--bg-hover);
16251625 color: var(--accent-blue);
16261626}
1627
1628/* Contact List - Multi-Address Support */
1629.contact-addresses {
1630 display: flex;
1631 flex-direction: column;
1632 gap: 8px;
1633 margin: 12px 0;
1634}
1635
1636.contact-address-row {
1637 display: flex;
1638 align-items: center;
1639 justify-content: space-between;
1640 gap: 8px;
1641 padding: 8px;
1642 background: var(--bg-secondary);
1643 border-radius: 8px;
1644 border: 1px solid var(--border-color);
1645}
1646
1647.contact-address-info {
1648 display: flex;
1649 align-items: center;
1650 gap: 8px;
1651 flex: 1;
1652 min-width: 0;
1653}
1654
1655.chain-icon {
1656 font-size: 18px;
1657 min-width: 24px;
1658 text-align: center;
1659}
1660
1661.contact-address {
1662 font-family: 'Courier New', monospace;
1663 font-size: 12px;
1664 color: var(--text-secondary);
1665 background: var(--bg-card);
1666 padding: 4px 8px;
1667 border-radius: 4px;
1668 cursor: pointer;
1669 flex: 1;
1670 overflow: hidden;
1671 text-overflow: ellipsis;
1672 white-space: nowrap;
1673}
1674
1675.contact-address:hover {
1676 color: var(--accent-blue);
1677 background: var(--bg-hover);
1678}
1679
1680.chain-badge {
1681 font-size: 10px;
1682 font-weight: 600;
1683 padding: 2px 8px;
1684 border-radius: 12px;
1685 text-transform: uppercase;
1686}
1687
1688.chain-badge-bitcoin {
1689 background: #f7931a;
1690 color: #000;
1691}
1692
1693.chain-badge-ethereum {
1694 background: #627eea;
1695 color: #fff;
1696}
1697
1698.contact-error {
1699 color: var(--accent-red);
1700 font-size: 14px;
1701 text-align: center;
1702 padding: 12px;
1703}
1704
1705.contact-card {
1706 background: var(--bg-card);
1707 border-radius: 12px;
1708 padding: 16px;
1709 border: 1px solid var(--border-color);
1710 transition: all 0.2s;
1711}
1712
1713.contact-card:hover {
1714 border-color: var(--accent-blue);
1715 box-shadow: var(--shadow);
1716}
1717
1718.contact-header {
1719 display: flex;
1720 align-items: center;
1721 justify-content: space-between;
1722 margin-bottom: 12px;
1723}
1724
1725.contact-name {
1726 font-size: 18px;
1727 font-weight: 600;
1728 color: var(--text-primary);
1729}
1730
1731.contact-actions-inline {
1732 display: flex;
1733 gap: 4px;
1734}
1735
1736.contact-notes {
1737 color: var(--text-secondary);
1738 font-size: 14px;
1739 margin-top: 12px;
1740 padding-top: 12px;
1741 border-top: 1px solid var(--border-color);
1742}
1743
1744.contacts-grid {
1745 display: grid;
1746 gap: 16px;
1747 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1748}
1749
1750@media (max-width: 768px) {
1751 .contacts-grid {
1752 grid-template-columns: 1fr;
1753 }
1754}