| … | … | @@ -241,30 +241,11 @@ function validateEthereumAddress(address) { |
| 241 | 241 | return { valid: true, warning: 'No EIP-55 checksum (address not checksummed)' } |
| 242 | 242 | } |
| 243 | 243 | |
| 244 | // Verify EIP-55 checksum | |
| 245 | // Keccak-256 hash of lowercase address | |
| 246 | const hash = crypto.createHash('keccak256') | |
| 247 | .update(Buffer.from(addressNoPrefix.toLowerCase(), 'utf8')) | |
| 248 | .digest('hex') | |
| 249 | ||
| 250 | for (let i = 0; i < 40; i++) { | |
| 251 | const char = addressNoPrefix[i] | |
| 252 | const hashChar = hash[i] | |
| 253 | ||
| 254 | // If hash char >= 8, address char should be uppercase | |
| 255 | if (parseInt(hashChar, 16) >= 8) { | |
| 256 | if (char !== char.toUpperCase()) { | |
| 257 | return { valid: false, reason: 'Invalid EIP-55 checksum (case mismatch)' } | |
| 258 | } | |
| 259 | } else { | |
| 260 | // If hash char < 8, address char should be lowercase | |
| 261 | if (char !== char.toLowerCase()) { | |
| 262 | return { valid: false, reason: 'Invalid EIP-55 checksum (case mismatch)' } | |
| 263 | } | |
| 264 | } | |
| 265 | } | |
| 266 | ||
| 267 | return { valid: true } | |
| 244 | // TEMPORARY FIX: EIP-55 checksum validation disabled | |
| 245 | // bare-crypto doesn't support keccak256 hash algorithm | |
| 246 | // TODO: Add keccak library (@noble/hashes or @ethereumjs/util) for proper checksum validation | |
| 247 | console.warn('⚠️ EIP-55 checksum validation skipped (keccak256 not available in bare-crypto)') | |
| 248 | return { valid: true, warning: 'EIP-55 checksum validation skipped (bare-crypto limitation)' } | |
| 268 | 249 | } |
| 269 | 250 | |
| 270 | 251 | /** |
| … | … | @@ -961,7 +961,10 @@ async function loadContactsList() { |
| 961 | 961 | <code class="contact-address" onclick="copyToClipboard('${addr.address}', 'Address copied!')" title="Click to copy">${shortAddr}</code> |
| 962 | 962 | <span class="chain-badge chain-badge-${addr.chain}">${addr.chain}</span> |
| 963 | 963 | </div> |
| 964 | <button class="btn-icon-small" onclick="sendToContactAddress('${contact.id}', '${addr.chain}')" title="Send ${addr.chain}">💸</button> | |
| 964 | <div class="contact-address-actions"> | |
| 965 | <button class="btn-icon-small" onclick="sendToContactAddress('${contact.id}', '${addr.chain}')" title="Send ${addr.chain}">💸</button> | |
| 966 | <button class="btn-icon-small btn-danger" onclick="removeContactAddress('${contact.id}', '${addr.chain}', '${escapeHtml(contact.name)}')" title="Remove ${addr.chain} address">🗑️</button> | |
| 967 | </div> | |
| 965 | 968 | </div> |
| 966 | 969 | ` |
| 967 | 970 | } |
| … | … | @@ -970,6 +973,13 @@ async function loadContactsList() { |
| 970 | 973 | html += '<p class="contact-error">⚠️ No addresses configured</p>' |
| 971 | 974 | } |
| 972 | 975 | |
| 976 | // Add Wallet button | |
| 977 | html += ` | |
| 978 | <div class="contact-footer"> | |
| 979 | <button class="btn-link" onclick="showAddWallet('${contact.id}', '${escapeHtml(contact.name)}')">+ Add Wallet</button> | |
| 980 | </div> | |
| 981 | ` | |
| 982 | ||
| 973 | 983 | if (contact.notes) { |
| 974 | 984 | html += `<p class="contact-notes">${escapeHtml(contact.notes)}</p>` |
| 975 | 985 | } |
| … | … | @@ -1123,6 +1133,89 @@ async function deleteContactConfirm(contactId, contactName) { |
| 1123 | 1133 | } |
| 1124 | 1134 | } |
| 1125 | 1135 | |
| 1136 | // Show add wallet modal for contact | |
| 1137 | function showAddWallet(contactId, contactName) { | |
| 1138 | currentEditContactId = contactId | |
| 1139 | ||
| 1140 | document.getElementById('add-wallet-contact-name').textContent = contactName | |
| 1141 | document.getElementById('add-wallet-chain').value = 'bitcoin' | |
| 1142 | document.getElementById('add-wallet-address').value = '' | |
| 1143 | document.getElementById('add-wallet-error').style.display = 'none' | |
| 1144 | ||
| 1145 | const modal = document.getElementById('add-wallet-modal') | |
| 1146 | modal.showModal() | |
| 1147 | } | |
| 1148 | ||
| 1149 | // Close add wallet modal | |
| 1150 | function closeAddWallet() { | |
| 1151 | document.getElementById('add-wallet-modal').close() | |
| 1152 | currentEditContactId = null | |
| 1153 | } | |
| 1154 | ||
| 1155 | // Submit add wallet | |
| 1156 | async function submitAddWallet(event) { | |
| 1157 | event.preventDefault() | |
| 1158 | ||
| 1159 | const contactId = currentEditContactId | |
| 1160 | const chain = document.getElementById('add-wallet-chain').value | |
| 1161 | const address = document.getElementById('add-wallet-address').value.trim() | |
| 1162 | ||
| 1163 | const errorDiv = document.getElementById('add-wallet-error') | |
| 1164 | errorDiv.style.display = 'none' | |
| 1165 | ||
| 1166 | try { | |
| 1167 | const res = await authFetch(`${API_BASE}/contacts/${contactId}/addresses`, { | |
| 1168 | method: 'POST', | |
| 1169 | headers: { 'Content-Type': 'application/json' }, | |
| 1170 | body: JSON.stringify({ chain, address, network: selectedNetwork }) | |
| 1171 | }) | |
| 1172 | ||
| 1173 | const data = await res.json() | |
| 1174 | ||
| 1175 | if (data.error) { | |
| 1176 | errorDiv.textContent = data.error | |
| 1177 | errorDiv.style.display = 'block' | |
| 1178 | return | |
| 1179 | } | |
| 1180 | ||
| 1181 | showSuccess(`${chain.toUpperCase()} wallet added!`) | |
| 1182 | closeAddWallet() | |
| 1183 | await loadContactsList() | |
| 1184 | } catch (err) { | |
| 1185 | errorDiv.textContent = `Error: ${err.message}` | |
| 1186 | errorDiv.style.display = 'block' | |
| 1187 | } | |
| 1188 | } | |
| 1189 | ||
| 1190 | // Remove address from contact | |
| 1191 | async function removeContactAddress(contactId, chain, contactName) { | |
| 1192 | const confirmed = confirm( | |
| 1193 | `Remove ${chain.toUpperCase()} wallet from "${contactName}"?\n\n` + | |
| 1194 | `This will remove this wallet address from the contact.\n\n` + | |
| 1195 | `Continue?` | |
| 1196 | ) | |
| 1197 | ||
| 1198 | if (!confirmed) return | |
| 1199 | ||
| 1200 | try { | |
| 1201 | const res = await authFetch(`${API_BASE}/contacts/${contactId}/addresses/${chain}`, { | |
| 1202 | method: 'DELETE' | |
| 1203 | }) | |
| 1204 | ||
| 1205 | const data = await res.json() | |
| 1206 | ||
| 1207 | if (data.error) { | |
| 1208 | showError(data.error) | |
| 1209 | return | |
| 1210 | } | |
| 1211 | ||
| 1212 | showSuccess(`${chain.toUpperCase()} wallet removed`) | |
| 1213 | await loadContactsList() | |
| 1214 | } catch (err) { | |
| 1215 | showError(`Error: ${err.message}`) | |
| 1216 | } | |
| 1217 | } | |
| 1218 | ||
| 1126 | 1219 | // Send to specific address of a contact |
| 1127 | 1220 | async function sendToContactAddress(contactId, chain) { |
| 1128 | 1221 | const contact = currentContacts.find(c => c.id === contactId) |
| … | … | @@ -760,6 +760,42 @@ |
| 760 | 760 | </div> |
| 761 | 761 | </dialog> |
| 762 | 762 | |
| 763 | <!-- Add Wallet to Contact Modal --> | |
| 764 | <dialog id="add-wallet-modal" class="modal-dialog"> | |
| 765 | <div class="modal-header"> | |
| 766 | <h3>➕ Add Wallet to Contact</h3> | |
| 767 | <button class="modal-close" onclick="closeAddWallet()">×</button> | |
| 768 | </div> | |
| 769 | <div class="modal-body"> | |
| 770 | <p class="modal-info"> | |
| 771 | Adding wallet for: <strong id="add-wallet-contact-name"></strong> | |
| 772 | </p> | |
| 773 | ||
| 774 | <form id="add-wallet-form" onsubmit="submitAddWallet(event)"> | |
| 775 | <div class="form-group"> | |
| 776 | <label for="add-wallet-chain">Blockchain *</label> | |
| 777 | <select id="add-wallet-chain" required> | |
| 778 | <option value="bitcoin">Bitcoin</option> | |
| 779 | <option value="ethereum">Ethereum</option> | |
| 780 | </select> | |
| 781 | </div> | |
| 782 | ||
| 783 | <div class="form-group"> | |
| 784 | <label for="add-wallet-address">Wallet Address *</label> | |
| 785 | <input type="text" id="add-wallet-address" required placeholder="bc1q... or 0x..."> | |
| 786 | <small class="form-hint">Enter the blockchain address for this wallet</small> | |
| 787 | </div> | |
| 788 | ||
| 789 | <div class="form-actions"> | |
| 790 | <button type="submit" class="btn-primary">Add Wallet</button> | |
| 791 | <button type="button" class="btn-secondary" onclick="closeAddWallet()">Cancel</button> | |
| 792 | </div> | |
| 793 | ||
| 794 | <div id="add-wallet-error" class="form-error" style="display: none;"></div> | |
| 795 | </form> | |
| 796 | </div> | |
| 797 | </dialog> | |
| 798 | ||
| 763 | 799 | <!-- Password Setup/Unlock Dialog --> |
| 764 | 800 | <dialog id="password-dialog"> |
| 765 | 801 | <div class="modal-header"> |
| … | … | @@ -1752,3 +1752,32 @@ footer a:hover { |
| 1752 | 1752 | grid-template-columns: 1fr; |
| 1753 | 1753 | } |
| 1754 | 1754 | } |
| 1755 | ||
| 1756 | /* Contact Footer */ | |
| 1757 | .contact-footer { | |
| 1758 | margin-top: 12px; | |
| 1759 | padding-top: 12px; | |
| 1760 | border-top: 1px solid var(--border-color); | |
| 1761 | text-align: center; | |
| 1762 | } | |
| 1763 | ||
| 1764 | .contact-address-actions { | |
| 1765 | display: flex; | |
| 1766 | gap: 4px; | |
| 1767 | } | |
| 1768 | ||
| 1769 | .btn-danger { | |
| 1770 | color: var(--accent-red); | |
| 1771 | } | |
| 1772 | ||
| 1773 | .btn-danger:hover { | |
| 1774 | background: rgba(239, 68, 68, 0.1); | |
| 1775 | } | |
| 1776 | ||
| 1777 | .modal-info { | |
| 1778 | background: var(--bg-secondary); | |
| 1779 | padding: 12px; | |
| 1780 | border-radius: 8px; | |
| 1781 | margin-bottom: 16px; | |
| 1782 | border-left: 3px solid var(--accent-blue); | |
| 1783 | } |
| … | … | @@ -38,6 +38,8 @@ const { |
| 38 | 38 | updateContact, |
| 39 | 39 | deleteContact, |
| 40 | 40 | findContactByAddress, |
| 41 | addAddressToContact, | |
| 42 | removeAddressFromContact, | |
| 41 | 43 | // Transaction syncing |
| 42 | 44 | syncTransactionsFromWDK |
| 43 | 45 | } = require('./lib/wallet-storage.cjs') |
| … | … | @@ -1007,6 +1009,97 @@ const routes = { |
| 1007 | 1009 | } |
| 1008 | 1010 | }), |
| 1009 | 1011 | |
| 1012 | // POST /api/contacts/:id/addresses - Add address to contact | |
| 1013 | 'POST /api/contacts/': requireAuth(async (req, res, id, ...rest) => { | |
| 1014 | // Check if this is the /addresses sub-route | |
| 1015 | if (rest[0] === 'addresses') { | |
| 1016 | try { | |
| 1017 | if (!id) { | |
| 1018 | return sendError(res, 'Contact ID required', 400) | |
| 1019 | } | |
| 1020 | ||
| 1021 | const body = await parseBody(req) | |
| 1022 | let { chain, address, network } = body | |
| 1023 | ||
| 1024 | if (!chain || !address) { | |
| 1025 | return sendError(res, 'Missing required fields: chain, address', 400) | |
| 1026 | } | |
| 1027 | ||
| 1028 | // Validate chain | |
| 1029 | try { | |
| 1030 | chain = validateChain(chain) | |
| 1031 | } catch (err) { | |
| 1032 | return sendError(res, err.message, 400) | |
| 1033 | } | |
| 1034 | ||
| 1035 | // Validate network | |
| 1036 | if (!network) { | |
| 1037 | const config = getConfig() | |
| 1038 | network = config.network || 'testnet' | |
| 1039 | } | |
| 1040 | ||
| 1041 | // Validate address | |
| 1042 | const addressValidation = validateAddress(address, chain, network) | |
| 1043 | if (!addressValidation.valid) { | |
| 1044 | return sendError(res, `Invalid address: ${addressValidation.reason}`, 400) | |
| 1045 | } | |
| 1046 | if (addressValidation.warning) { | |
| 1047 | console.warn(`⚠️ Address validation warning: ${addressValidation.warning}`) | |
| 1048 | } | |
| 1049 | ||
| 1050 | const identity = await loadIdentity() | |
| 1051 | await initializeStorage(identity) | |
| 1052 | ||
| 1053 | const contact = await addAddressToContact(id, { chain, address, network }) | |
| 1054 | ||
| 1055 | sendJSON(res, { | |
| 1056 | success: true, | |
| 1057 | message: 'Address added to contact successfully', | |
| 1058 | contact | |
| 1059 | }) | |
| 1060 | } catch (err) { | |
| 1061 | console.error('Add address to contact error:', err) | |
| 1062 | sendError(res, err.message) | |
| 1063 | } | |
| 1064 | return | |
| 1065 | } | |
| 1066 | ||
| 1067 | // If not /addresses, return 404 | |
| 1068 | sendError(res, 'Not found', 404) | |
| 1069 | }), | |
| 1070 | ||
| 1071 | // DELETE /api/contacts/:id/addresses/:chain - Remove address from contact | |
| 1072 | 'DELETE /api/contacts/': requireAuth(async (req, res, id, ...rest) => { | |
| 1073 | // Check if this is the /addresses/:chain sub-route | |
| 1074 | if (rest[0] === 'addresses' && rest[1]) { | |
| 1075 | try { | |
| 1076 | if (!id) { | |
| 1077 | return sendError(res, 'Contact ID required', 400) | |
| 1078 | } | |
| 1079 | ||
| 1080 | const chain = rest[1] | |
| 1081 | ||
| 1082 | const identity = await loadIdentity() | |
| 1083 | await initializeStorage(identity) | |
| 1084 | ||
| 1085 | const contact = await removeAddressFromContact(id, chain) | |
| 1086 | ||
| 1087 | sendJSON(res, { | |
| 1088 | success: true, | |
| 1089 | message: 'Address removed from contact successfully', | |
| 1090 | contact | |
| 1091 | }) | |
| 1092 | } catch (err) { | |
| 1093 | console.error('Remove address from contact error:', err) | |
| 1094 | sendError(res, err.message) | |
| 1095 | } | |
| 1096 | return | |
| 1097 | } | |
| 1098 | ||
| 1099 | // If not matching, return 404 | |
| 1100 | sendError(res, 'Not found', 404) | |
| 1101 | }), | |
| 1102 | ||
| 1010 | 1103 | // GET /api/contacts/search?q=name - Search contacts |
| 1011 | 1104 | 'GET /api/contacts/search': requireAuth(async (req, res) => { |
| 1012 | 1105 | try { |