OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop commits / 0b2169e

Fix: convert amount input from human-readable to smallest units (2 USDC → 2000000)
Deployment: testing (hyper-wallet-desktop-test) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
0b2169e06344128aad39951ed55d89f7148ca4d6
by plankatron <fleeky@gmail.com> · 2026-01-30T03:34:05.000Z

Changed files (2) +112 -20

.claude/deployment-history.json +2 -2
……@@ -4,7 +4,7 @@
44 "branch": "testing",
55 "version": "unknown",
66 "pear_key": "pear://unknown",
7 "deployed_at": "2026-01-29T22:16:36-05:00",
8 "commit_message": "Fix: sync transactions uses current account index instead of hardcoded 0",
7 "deployed_at": "2026-01-29T22:20:40-05:00",
8 "commit_message": "Fix: add USDC sync support and display token amounts correctly in transactions",
99 "tmux": "pear-seed:5"
1010}
desktop/wallet.js +110 -18
……@@ -2043,6 +2043,54 @@ function formatEthFee(wei) {
20432043 return `${eth.toFixed(6)} ETH`
20442044}
20452045
2046// Convert human-readable amount to smallest units (satoshi/wei/token units)
2047// This is needed because users type "2" meaning 2 USDC, but backend expects 2000000
2048function toSmallestUnits(amount, chain) {
2049 const num = parseFloat(amount)
2050 if (isNaN(num) || num < 0) {
2051 return '0'
2052 }
2053
2054 if (chain === 'bitcoin') {
2055 // BTC to satoshi (1e8)
2056 return Math.floor(num * 1e8).toString()
2057 } else if (chain.includes('usdt') || chain.includes('usdc')) {
2058 // USDT/USDC have 6 decimals
2059 return Math.floor(num * 1e6).toString()
2060 } else {
2061 // ETH to wei (1e18) - use BigInt to avoid precision loss
2062 // For large numbers, we need to be careful with floating point
2063 const wholePart = Math.floor(num)
2064 const fractionalPart = num - wholePart
2065 const wholeWei = BigInt(wholePart) * BigInt(1e18)
2066 const fractionalWei = BigInt(Math.floor(fractionalPart * 1e18))
2067 return (wholeWei + fractionalWei).toString()
2068 }
2069}
2070
2071// Convert smallest units to human-readable amount
2072// This is used when displaying amounts or setting input values
2073function fromSmallestUnits(amount, chain) {
2074 const num = Number(amount)
2075 if (isNaN(num) || num < 0) {
2076 return '0'
2077 }
2078
2079 if (chain === 'bitcoin') {
2080 // Satoshi to BTC (1e8)
2081 const btc = num / 1e8
2082 return btc.toFixed(8).replace(/\.?0+$/, '')
2083 } else if (chain.includes('usdt') || chain.includes('usdc')) {
2084 // USDT/USDC have 6 decimals
2085 const tokens = num / 1e6
2086 return tokens.toFixed(6).replace(/\.?0+$/, '')
2087 } else {
2088 // Wei to ETH (1e18)
2089 const eth = num / 1e18
2090 return eth.toFixed(18).replace(/\.?0+$/, '')
2091 }
2092}
2093
20462094// Show error toast
20472095function showError(message) {
20482096 const toast = document.getElementById('error-toast')
……@@ -2906,7 +2954,9 @@ async function setMaxAmount() {
29062954
29072955 console.log('Setting max amount (static reserve):', { maxAmount, reserved, currentBalance })
29082956
2909 document.getElementById('send-amount').value = maxAmount
2957 // Convert to human-readable for input display
2958 const maxAmountDisplay = fromSmallestUnits(maxAmount, currentSendChain)
2959 document.getElementById('send-amount').value = maxAmountDisplay
29102960 document.getElementById('fee-display').innerHTML = `
29112961 <p class="help-text" style="color: #f59e0b;">
29122962 ℹ️ Estimated max amount (enter recipient address for exact fee calculation)
……@@ -2959,8 +3009,11 @@ async function setMaxAmount() {
29593009
29603010 console.log('Setting max amount (exact fee):', { maxAmount, fee: estimatedFee, currentBalance })
29613011
3012 // Convert to human-readable for input display
3013 const maxAmountDisplay = fromSmallestUnits(maxAmount, currentSendChain)
3014
29623015 // Set the max amount
2963 document.getElementById('send-amount').value = maxAmount
3016 document.getElementById('send-amount').value = maxAmountDisplay
29643017 usedMaxAmount = true // Mark that max was used
29653018
29663019 // Display fee info
……@@ -2975,8 +3028,7 @@ async function setMaxAmount() {
29753028 totalDisplay = `${currentBalance.toLocaleString()} sats`
29763029 } else if (isToken) {
29773030 const tokenSymbol = currentSendChain.includes('usdt') ? 'USDT' : 'USDC'
2978 const tokenAmount = Number(maxAmount) / (currentSendChain.includes('usdt') ? 1e6 : 1e6)
2979 totalDisplay = `${tokenAmount.toFixed(2)} ${tokenSymbol} + ${feeFormatted} gas`
3031 totalDisplay = `${maxAmountDisplay} ${tokenSymbol} + ${feeFormatted} gas`
29803032 } else {
29813033 // Native ETH
29823034 const totalEth = Number(currentBalance) / 1e18
……@@ -3010,9 +3062,9 @@ async function setMaxAmount() {
30103062// Estimate transaction fee
30113063async function estimateFee() {
30123064 const toAddress = document.getElementById('send-to').value
3013 const amount = document.getElementById('send-amount').value
3065 const amountInput = document.getElementById('send-amount').value
30143066
3015 if (!toAddress || !amount) {
3067 if (!toAddress || !amountInput) {
30163068 document.getElementById('fee-display').innerHTML =
30173069 '<p class="help-text">Enter address and amount to estimate fee</p>'
30183070 return
……@@ -3020,6 +3072,9 @@ async function estimateFee() {
30203072
30213073 document.getElementById('fee-display').innerHTML = '<div class="spinner"></div>'
30223074
3075 // Convert human-readable amount to smallest units for backend
3076 const amount = toSmallestUnits(amountInput, currentSendChain)
3077
30233078 try {
30243079 const res = await authFetch(`/api/estimate-fee`, {
30253080 method: 'POST',
……@@ -3051,10 +3106,13 @@ async function estimateFee() {
30513106 const isToken = currentSendChain.includes('usdt') || currentSendChain.includes('usdc')
30523107 let totalDisplay
30533108 if (currentSendChain === 'bitcoin') {
3054 totalDisplay = `${Number(amount) + Number(data.fee)} sats`
3109 // Amount is now in sats (smallest units)
3110 const totalSats = Number(amount) + Number(data.fee)
3111 totalDisplay = `${totalSats.toLocaleString()} sats`
30553112 } else if (isToken) {
30563113 const tokenSymbol = currentSendChain.includes('usdt') ? 'USDT' : 'USDC'
3057 totalDisplay = `${amount} ${tokenSymbol} + ${feeFormatted} gas`
3114 // Display human-readable amount from input
3115 totalDisplay = `${amountInput} ${tokenSymbol} + ${feeFormatted} gas`
30583116 } else {
30593117 // Native ETH - convert both to ETH for total
30603118 const totalWei = BigInt(amount) + BigInt(data.fee)
……@@ -3083,7 +3141,7 @@ async function estimateFee() {
30833141// Validate and show confirmation
30843142function confirmSend() {
30853143 const toAddress = document.getElementById('send-to').value.trim()
3086 const amount = document.getElementById('send-amount').value.trim()
3144 const amountInput = document.getElementById('send-amount').value.trim()
30873145
30883146 // Clear previous errors
30893147 document.getElementById('send-to-error').textContent = ''
……@@ -3106,11 +3164,12 @@ function confirmSend() {
31063164 }
31073165 }
31083166
3109 // Validate amount
3110 if (!amount || isNaN(amount) || Number(amount) <= 0) {
3167 // Validate amount - convert to smallest units for balance comparison
3168 const amountInSmallestUnits = Number(toSmallestUnits(amountInput, currentSendChain))
3169 if (!amountInput || isNaN(parseFloat(amountInput)) || parseFloat(amountInput) <= 0) {
31113170 document.getElementById('send-amount-error').textContent = 'Invalid amount'
31123171 hasError = true
3113 } else if (Number(amount) > currentBalance) {
3172 } else if (amountInSmallestUnits > currentBalance) {
31143173 document.getElementById('send-amount-error').textContent = 'Insufficient balance'
31153174 hasError = true
31163175 }
……@@ -3130,13 +3189,39 @@ function confirmSend() {
31303189 const confirmModal = document.getElementById('confirm-modal')
31313190 const confirmContent = document.getElementById('confirm-content')
31323191
3133 const amountFormatted = currentSendChain === 'bitcoin'
3134 ? formatBTC(amount)
3135 : formatETH(amount)
3192 // Convert to smallest units for calculations
3193 const amountSmallest = toSmallestUnits(amountInput, currentSendChain)
3194
3195 // Format for display
3196 const isToken = currentSendChain.includes('usdt') || currentSendChain.includes('usdc')
3197 let amountFormatted
3198 if (currentSendChain === 'bitcoin') {
3199 amountFormatted = formatBTC(amountSmallest)
3200 } else if (isToken) {
3201 const tokenSymbol = currentSendChain.includes('usdt') ? 'USDT' : 'USDC'
3202 amountFormatted = `${amountInput} ${tokenSymbol}`
3203 } else {
3204 amountFormatted = formatETH(amountSmallest)
3205 }
3206
31363207 const feeFormatted = currentSendChain === 'bitcoin'
31373208 ? formatSats(estimatedFee)
31383209 : formatEthFee(estimatedFee)
31393210
3211 // Calculate total display
3212 let totalDisplay
3213 if (currentSendChain === 'bitcoin') {
3214 const totalSats = Number(amountSmallest) + Number(estimatedFee)
3215 totalDisplay = `${totalSats.toLocaleString()} sats`
3216 } else if (isToken) {
3217 const tokenSymbol = currentSendChain.includes('usdt') ? 'USDT' : 'USDC'
3218 totalDisplay = `${amountInput} ${tokenSymbol} + ${feeFormatted} gas`
3219 } else {
3220 const totalWei = BigInt(amountSmallest) + BigInt(estimatedFee)
3221 const totalEth = Number(totalWei) / 1e18
3222 totalDisplay = `${totalEth.toFixed(6)} ETH`
3223 }
3224
31403225 const networkWarning = selectedNetwork === 'mainnet'
31413226 ? '<div class="confirm-warning">⚠️ WARNING: You are sending on MAINNET. This will use real funds!</div>'
31423227 : ''
……@@ -3161,7 +3246,7 @@ function confirmSend() {
31613246 </div>
31623247 <div class="confirm-row">
31633248 <span class="confirm-label">Total:</span>
3164 <span class="confirm-value">${Number(amount) + Number(estimatedFee)} ${currentSendChain === 'bitcoin' ? 'sats' : 'wei'}</span>
3249 <span class="confirm-value">${totalDisplay}</span>
31653250 </div>
31663251 `
31673252
……@@ -3176,7 +3261,10 @@ function closeConfirm() {
31763261// Execute send transaction
31773262async function executeSend() {
31783263 const toAddress = document.getElementById('send-to').value.trim()
3179 const amount = document.getElementById('send-amount').value.trim()
3264 const amountInput = document.getElementById('send-amount').value.trim()
3265
3266 // Convert human-readable amount to smallest units for backend
3267 const amount = toSmallestUnits(amountInput, currentSendChain)
31803268
31813269 // Close confirmation modal
31823270 closeConfirm()
……@@ -3210,9 +3298,13 @@ async function executeSend() {
32103298
32113299 // Show native notification (don't let this crash the app)
32123300 try {
3301 const isToken = currentSendChain.includes('usdt') || currentSendChain.includes('usdc')
3302 const displayUnit = isToken
3303 ? (currentSendChain.includes('usdt') ? 'USDT' : 'USDC')
3304 : currentSendChain.toUpperCase()
32133305 showNotification(
32143306 'Transaction Sent',
3215 `${amount} ${currentSendChain.toUpperCase()} sent successfully`,
3307 `${amountInput} ${displayUnit} sent successfully`,
32163308 'success'
32173309 )
32183310 } catch (notifErr) {