| … | … | @@ -30,9 +30,9 @@ |
| 30 | 30 | "persistent": true, |
| 31 | 31 | "channel": "hyper-wallet-test", |
| 32 | 32 | "pear_key": "pear://6bafdga5tc9wpts69y478sgnsght4wsuhthroa1a3oxn1xcbxqmo", |
| 33 | "latest_version": "7665", | |
| 33 | "latest_version": "7669", | |
| 34 | 34 | "last_deployed": "2026-01-27", |
| 35 | "features": "Ethereum validation fixed + Add/Remove wallets to contacts + Multi-address support", | |
| 35 | "features": "Fixed: Change password (pbkdf2) + Encryption exports + Ethereum validation + Add/Remove wallets", | |
| 36 | 36 | "notes": "Deploy here first with --testing flag, verify, then promote to production (window 0)" |
| 37 | 37 | } |
| 38 | 38 | } |
| … | … | @@ -17,11 +17,12 @@ |
| 17 | 17 | * |
| 18 | 18 | * VALIDATION: |
| 19 | 19 | * - Bitcoin: Base58Check (P2PKH, P2SH) and Bech32/Bech32m (SegWit) |
| 20 | * - Ethereum: EIP-55 mixed-case checksum | |
| 20 | * - Ethereum: EIP-55 mixed-case checksum (keccak256) | |
| 21 | 21 | * - Returns: { valid: boolean, reason?: string, warning?: string } |
| 22 | 22 | */ |
| 23 | 23 | |
| 24 | 24 | const crypto = require('bare-crypto') |
| 25 | const { keccak_256 } = require('@noble/hashes/sha3') | |
| 25 | 26 | |
| 26 | 27 | /** |
| 27 | 28 | * Bitcoin Base58 alphabet (no 0, O, I, l to avoid confusion) |
| … | … | @@ -241,11 +242,34 @@ function validateEthereumAddress(address) { |
| 241 | 242 | return { valid: true, warning: 'No EIP-55 checksum (address not checksummed)' } |
| 242 | 243 | } |
| 243 | 244 | |
| 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)' } | |
| 245 | // Verify EIP-55 checksum using keccak256 | |
| 246 | try { | |
| 247 | // Hash the lowercase address | |
| 248 | const hash = keccak_256(addressNoPrefix.toLowerCase()) | |
| 249 | const hashHex = Buffer.from(hash).toString('hex') | |
| 250 | ||
| 251 | // Check each character | |
| 252 | for (let i = 0; i < 40; i++) { | |
| 253 | const char = addressNoPrefix[i] | |
| 254 | const hashChar = hashHex[i] | |
| 255 | ||
| 256 | // If hash digit >= 8, char should be uppercase; otherwise lowercase | |
| 257 | if (parseInt(hashChar, 16) >= 8) { | |
| 258 | if (char !== char.toUpperCase()) { | |
| 259 | return { valid: false, reason: 'Invalid EIP-55 checksum (character case mismatch)' } | |
| 260 | } | |
| 261 | } else { | |
| 262 | if (char !== char.toLowerCase()) { | |
| 263 | return { valid: false, reason: 'Invalid EIP-55 checksum (character case mismatch)' } | |
| 264 | } | |
| 265 | } | |
| 266 | } | |
| 267 | ||
| 268 | return { valid: true } | |
| 269 | } catch (err) { | |
| 270 | console.error('EIP-55 checksum validation error:', err) | |
| 271 | return { valid: true, warning: 'EIP-55 checksum validation failed, accepting address' } | |
| 272 | } | |
| 249 | 273 | } |
| 250 | 274 | |
| 251 | 275 | /** |
| … | … | @@ -1,13 +1,14 @@ |
| 1 | 1 | { |
| 2 | "name": "wdk-bare-test", | |
| 2 | "name": "hyper-wallet", | |
| 3 | 3 | "version": "0.1.0", |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | "name": "wdk-bare-test", | |
| 8 | "name": "hyper-wallet", | |
| 9 | 9 | "hasInstallScript": true, |
| 10 | 10 | "dependencies": { |
| 11 | "@noble/hashes": "^2.0.1", | |
| 11 | 12 | "@tetherto/wdk": "^1.0.0-beta.5", |
| 12 | 13 | "@tetherto/wdk-wallet-btc": "^1.0.0-beta.4", |
| 13 | 14 | "@tetherto/wdk-wallet-evm": "^1.0.0-beta.4", |
| … | … | @@ -121,7 +122,7 @@ |
| 121 | 122 | "url": "https://paulmillr.com/funding/" |
| 122 | 123 | } |
| 123 | 124 | }, |
| 124 | "node_modules/@noble/hashes": { | |
| 125 | "node_modules/@noble/curves/node_modules/@noble/hashes": { | |
| 125 | 126 | "version": "1.8.0", |
| 126 | 127 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", |
| 127 | 128 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", |
| … | … | @@ -133,6 +134,18 @@ |
| 133 | 134 | "url": "https://paulmillr.com/funding/" |
| 134 | 135 | } |
| 135 | 136 | }, |
| 137 | "node_modules/@noble/hashes": { | |
| 138 | "version": "2.0.1", | |
| 139 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", | |
| 140 | "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", | |
| 141 | "license": "MIT", | |
| 142 | "engines": { | |
| 143 | "node": ">= 20.19.0" | |
| 144 | }, | |
| 145 | "funding": { | |
| 146 | "url": "https://paulmillr.com/funding/" | |
| 147 | } | |
| 148 | }, | |
| 136 | 149 | "node_modules/@noble/secp256k1": { |
| 137 | 150 | "version": "2.2.3", |
| 138 | 151 | "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.2.3.tgz", |
| … | … | @@ -223,6 +236,18 @@ |
| 223 | 236 | "url": "https://paulmillr.com/funding/" |
| 224 | 237 | } |
| 225 | 238 | }, |
| 239 | "node_modules/@tetherto/wdk-wallet-evm/node_modules/@noble/hashes": { | |
| 240 | "version": "1.8.0", | |
| 241 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", | |
| 242 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", | |
| 243 | "license": "MIT", | |
| 244 | "engines": { | |
| 245 | "node": "^14.21.3 || >=16" | |
| 246 | }, | |
| 247 | "funding": { | |
| 248 | "url": "https://paulmillr.com/funding/" | |
| 249 | } | |
| 250 | }, | |
| 226 | 251 | "node_modules/@types/node": { |
| 227 | 252 | "version": "22.7.5", |
| 228 | 253 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", |
| … | … | @@ -1288,6 +1313,18 @@ |
| 1288 | 1313 | "node": ">=6.0.0" |
| 1289 | 1314 | } |
| 1290 | 1315 | }, |
| 1316 | "node_modules/bip32/node_modules/@noble/hashes": { | |
| 1317 | "version": "1.8.0", | |
| 1318 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", | |
| 1319 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", | |
| 1320 | "license": "MIT", | |
| 1321 | "engines": { | |
| 1322 | "node": "^14.21.3 || >=16" | |
| 1323 | }, | |
| 1324 | "funding": { | |
| 1325 | "url": "https://paulmillr.com/funding/" | |
| 1326 | } | |
| 1327 | }, | |
| 1291 | 1328 | "node_modules/bip39": { |
| 1292 | 1329 | "version": "3.1.0", |
| 1293 | 1330 | "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", |
| … | … | @@ -1308,6 +1345,18 @@ |
| 1308 | 1345 | "sodium-universal": "^5.0.1" |
| 1309 | 1346 | } |
| 1310 | 1347 | }, |
| 1348 | "node_modules/bip39/node_modules/@noble/hashes": { | |
| 1349 | "version": "1.8.0", | |
| 1350 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", | |
| 1351 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", | |
| 1352 | "license": "MIT", | |
| 1353 | "engines": { | |
| 1354 | "node": "^14.21.3 || >=16" | |
| 1355 | }, | |
| 1356 | "funding": { | |
| 1357 | "url": "https://paulmillr.com/funding/" | |
| 1358 | } | |
| 1359 | }, | |
| 1311 | 1360 | "node_modules/bip66": { |
| 1312 | 1361 | "version": "1.1.5", |
| 1313 | 1362 | "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", |
| … | … | @@ -1343,6 +1392,18 @@ |
| 1343 | 1392 | "node": ">=8.0.0" |
| 1344 | 1393 | } |
| 1345 | 1394 | }, |
| 1395 | "node_modules/bitcoinjs-lib/node_modules/@noble/hashes": { | |
| 1396 | "version": "1.8.0", | |
| 1397 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", | |
| 1398 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", | |
| 1399 | "license": "MIT", | |
| 1400 | "engines": { | |
| 1401 | "node": "^14.21.3 || >=16" | |
| 1402 | }, | |
| 1403 | "funding": { | |
| 1404 | "url": "https://paulmillr.com/funding/" | |
| 1405 | } | |
| 1406 | }, | |
| 1346 | 1407 | "node_modules/bitcoinjs-message": { |
| 1347 | 1408 | "version": "2.2.0", |
| 1348 | 1409 | "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", |
| … | … | @@ -1475,6 +1536,18 @@ |
| 1475 | 1536 | "bs58": "^5.0.0" |
| 1476 | 1537 | } |
| 1477 | 1538 | }, |
| 1539 | "node_modules/bs58check/node_modules/@noble/hashes": { | |
| 1540 | "version": "1.8.0", | |
| 1541 | "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", | |
| 1542 | "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", | |
| 1543 | "license": "MIT", | |
| 1544 | "engines": { | |
| 1545 | "node": "^14.21.3 || >=16" | |
| 1546 | }, | |
| 1547 | "funding": { | |
| 1548 | "url": "https://paulmillr.com/funding/" | |
| 1549 | } | |
| 1550 | }, | |
| 1478 | 1551 | "node_modules/buffer-equals": { |
| 1479 | 1552 | "version": "1.0.4", |
| 1480 | 1553 | "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", |
| … | … | @@ -30,6 +30,7 @@ |
| 30 | 30 | "postinstall": "bash scripts/setup-pear-compat.sh" |
| 31 | 31 | }, |
| 32 | 32 | "dependencies": { |
| 33 | "@noble/hashes": "^2.0.1", | |
| 33 | 34 | "@tetherto/wdk": "^1.0.0-beta.5", |
| 34 | 35 | "@tetherto/wdk-wallet-btc": "^1.0.0-beta.4", |
| 35 | 36 | "@tetherto/wdk-wallet-evm": "^1.0.0-beta.4", |
| … | … | @@ -184,6 +184,16 @@ document.addEventListener('DOMContentLoaded', () => { |
| 184 | 184 | confirmBtn.disabled = !e.target.checked |
| 185 | 185 | }) |
| 186 | 186 | } |
| 187 | ||
| 188 | // Event delegation for copying contact addresses | |
| 189 | document.addEventListener('click', (e) => { | |
| 190 | if (e.target.classList.contains('copyable-address')) { | |
| 191 | const address = e.target.getAttribute('data-address') | |
| 192 | if (address) { | |
| 193 | copyToClipboard(address, 'Address copied!') | |
| 194 | } | |
| 195 | } | |
| 196 | }) | |
| 187 | 197 | }) |
| 188 | 198 | |
| 189 | 199 | function confirmMnemonicBackup() { |
| … | … | @@ -958,7 +968,7 @@ async function loadContactsList() { |
| 958 | 968 | <div class="contact-address-row"> |
| 959 | 969 | <div class="contact-address-info"> |
| 960 | 970 | <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> | |
| 971 | <code class="contact-address copyable-address" data-address="${escapeHtml(addr.address)}" title="Click to copy full address">${shortAddr}</code> | |
| 962 | 972 | <span class="chain-badge chain-badge-${addr.chain}">${addr.chain}</span> |
| 963 | 973 | </div> |
| 964 | 974 | <div class="contact-address-actions"> |
| … | … | @@ -1011,24 +1011,33 @@ const routes = { |
| 1011 | 1011 | |
| 1012 | 1012 | // POST /api/contacts/:id/addresses - Add address to contact |
| 1013 | 1013 | 'POST /api/contacts/': requireAuth(async (req, res, id, ...rest) => { |
| 1014 | console.log('📝 POST /api/contacts/ route hit:', { id, rest }) | |
| 1015 | ||
| 1014 | 1016 | // Check if this is the /addresses sub-route |
| 1015 | 1017 | if (rest[0] === 'addresses') { |
| 1018 | console.log('✅ /addresses sub-route detected') | |
| 1016 | 1019 | try { |
| 1017 | 1020 | if (!id) { |
| 1021 | console.log('❌ No contact ID provided') | |
| 1018 | 1022 | return sendError(res, 'Contact ID required', 400) |
| 1019 | 1023 | } |
| 1020 | 1024 | |
| 1021 | 1025 | const body = await parseBody(req) |
| 1026 | console.log('📦 Request body:', { chain: body.chain, address: body.address, network: body.network }) | |
| 1027 | ||
| 1022 | 1028 | let { chain, address, network } = body |
| 1023 | 1029 | |
| 1024 | 1030 | if (!chain || !address) { |
| 1031 | console.log('❌ Missing required fields:', { chain, address }) | |
| 1025 | 1032 | return sendError(res, 'Missing required fields: chain, address', 400) |
| 1026 | 1033 | } |
| 1027 | 1034 | |
| 1028 | 1035 | // Validate chain |
| 1029 | 1036 | try { |
| 1030 | 1037 | chain = validateChain(chain) |
| 1038 | console.log('✅ Chain validated:', chain) | |
| 1031 | 1039 | } catch (err) { |
| 1040 | console.log('❌ Chain validation error:', err.message) | |
| 1032 | 1041 | return sendError(res, err.message, 400) |
| 1033 | 1042 | } |
| 1034 | 1043 | |
| … | … | @@ -1037,20 +1046,28 @@ const routes = { |
| 1037 | 1046 | const config = getConfig() |
| 1038 | 1047 | network = config.network || 'testnet' |
| 1039 | 1048 | } |
| 1049 | console.log('🌐 Network:', network) | |
| 1040 | 1050 | |
| 1041 | 1051 | // Validate address |
| 1052 | console.log('🔍 Validating address:', { address, chain, network }) | |
| 1042 | 1053 | const addressValidation = validateAddress(address, chain, network) |
| 1054 | console.log('📋 Address validation result:', addressValidation) | |
| 1055 | ||
| 1043 | 1056 | if (!addressValidation.valid) { |
| 1057 | console.log('❌ Address validation failed:', addressValidation.reason) | |
| 1044 | 1058 | return sendError(res, `Invalid address: ${addressValidation.reason}`, 400) |
| 1045 | 1059 | } |
| 1046 | 1060 | if (addressValidation.warning) { |
| 1047 | 1061 | console.warn(`⚠️ Address validation warning: ${addressValidation.warning}`) |
| 1048 | 1062 | } |
| 1049 | 1063 | |
| 1064 | console.log('💾 Loading identity and initializing storage...') | |
| 1050 | 1065 | const identity = await loadIdentity() |
| 1051 | 1066 | await initializeStorage(identity) |
| 1052 | 1067 | |
| 1068 | console.log('➕ Adding address to contact...') | |
| 1053 | 1069 | const contact = await addAddressToContact(id, { chain, address, network }) |
| 1070 | console.log('✅ Address added successfully:', contact.id) | |
| 1054 | 1071 | |
| 1055 | 1072 | sendJSON(res, { |
| 1056 | 1073 | success: true, |
| … | … | @@ -1058,13 +1075,15 @@ const routes = { |
| 1058 | 1075 | contact |
| 1059 | 1076 | }) |
| 1060 | 1077 | } catch (err) { |
| 1061 | console.error('Add address to contact error:', err) | |
| 1078 | console.error('❌ Add address to contact error:', err) | |
| 1079 | console.error('Stack trace:', err.stack) | |
| 1062 | 1080 | sendError(res, err.message) |
| 1063 | 1081 | } |
| 1064 | 1082 | return |
| 1065 | 1083 | } |
| 1066 | 1084 | |
| 1067 | 1085 | // If not /addresses, return 404 |
| 1086 | console.log('❌ Not /addresses sub-route, returning 404') | |
| 1068 | 1087 | sendError(res, 'Not found', 404) |
| 1069 | 1088 | }), |
| 1070 | 1089 |