| … | … | @@ -2043,6 +2043,54 @@ function formatEthFee(wei) { |
| 2043 | 2043 | return `${eth.toFixed(6)} ETH` |
| 2044 | 2044 | } |
| 2045 | 2045 | |
| 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 |
| 2048 | function 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 |
| 2073 | function 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 | |
| 2046 | 2094 | // Show error toast |
| 2047 | 2095 | function showError(message) { |
| 2048 | 2096 | const toast = document.getElementById('error-toast') |
| … | … | @@ -2906,7 +2954,9 @@ async function setMaxAmount() { |
| 2906 | 2954 | |
| 2907 | 2955 | console.log('Setting max amount (static reserve):', { maxAmount, reserved, currentBalance }) |
| 2908 | 2956 | |
| 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 |
| 2910 | 2960 | document.getElementById('fee-display').innerHTML = ` |
| 2911 | 2961 | <p class="help-text" style="color: #f59e0b;"> |
| 2912 | 2962 | ℹ️ Estimated max amount (enter recipient address for exact fee calculation) |
| … | … | @@ -2959,8 +3009,11 @@ async function setMaxAmount() { |
| 2959 | 3009 | |
| 2960 | 3010 | console.log('Setting max amount (exact fee):', { maxAmount, fee: estimatedFee, currentBalance }) |
| 2961 | 3011 | |
| 3012 | // Convert to human-readable for input display |
| 3013 | const maxAmountDisplay = fromSmallestUnits(maxAmount, currentSendChain) |
| 3014 | |
| 2962 | 3015 | // Set the max amount |
| 2963 | | document.getElementById('send-amount').value = maxAmount |
| 3016 | document.getElementById('send-amount').value = maxAmountDisplay |
| 2964 | 3017 | usedMaxAmount = true // Mark that max was used |
| 2965 | 3018 | |
| 2966 | 3019 | // Display fee info |
| … | … | @@ -2975,8 +3028,7 @@ async function setMaxAmount() { |
| 2975 | 3028 | totalDisplay = `${currentBalance.toLocaleString()} sats` |
| 2976 | 3029 | } else if (isToken) { |
| 2977 | 3030 | 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` |
| 2980 | 3032 | } else { |
| 2981 | 3033 | // Native ETH |
| 2982 | 3034 | const totalEth = Number(currentBalance) / 1e18 |
| … | … | @@ -3010,9 +3062,9 @@ async function setMaxAmount() { |
| 3010 | 3062 | // Estimate transaction fee |
| 3011 | 3063 | async function estimateFee() { |
| 3012 | 3064 | const toAddress = document.getElementById('send-to').value |
| 3013 | | const amount = document.getElementById('send-amount').value |
| 3065 | const amountInput = document.getElementById('send-amount').value |
| 3014 | 3066 | |
| 3015 | | if (!toAddress || !amount) { |
| 3067 | if (!toAddress || !amountInput) { |
| 3016 | 3068 | document.getElementById('fee-display').innerHTML = |
| 3017 | 3069 | '<p class="help-text">Enter address and amount to estimate fee</p>' |
| 3018 | 3070 | return |
| … | … | @@ -3020,6 +3072,9 @@ async function estimateFee() { |
| 3020 | 3072 | |
| 3021 | 3073 | document.getElementById('fee-display').innerHTML = '<div class="spinner"></div>' |
| 3022 | 3074 | |
| 3075 | // Convert human-readable amount to smallest units for backend |
| 3076 | const amount = toSmallestUnits(amountInput, currentSendChain) |
| 3077 | |
| 3023 | 3078 | try { |
| 3024 | 3079 | const res = await authFetch(`/api/estimate-fee`, { |
| 3025 | 3080 | method: 'POST', |
| … | … | @@ -3051,10 +3106,13 @@ async function estimateFee() { |
| 3051 | 3106 | const isToken = currentSendChain.includes('usdt') || currentSendChain.includes('usdc') |
| 3052 | 3107 | let totalDisplay |
| 3053 | 3108 | 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` |
| 3055 | 3112 | } else if (isToken) { |
| 3056 | 3113 | 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` |
| 3058 | 3116 | } else { |
| 3059 | 3117 | // Native ETH - convert both to ETH for total |
| 3060 | 3118 | const totalWei = BigInt(amount) + BigInt(data.fee) |
| … | … | @@ -3083,7 +3141,7 @@ async function estimateFee() { |
| 3083 | 3141 | // Validate and show confirmation |
| 3084 | 3142 | function confirmSend() { |
| 3085 | 3143 | 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() |
| 3087 | 3145 | |
| 3088 | 3146 | // Clear previous errors |
| 3089 | 3147 | document.getElementById('send-to-error').textContent = '' |
| … | … | @@ -3106,11 +3164,12 @@ function confirmSend() { |
| 3106 | 3164 | } |
| 3107 | 3165 | } |
| 3108 | 3166 | |
| 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) { |
| 3111 | 3170 | document.getElementById('send-amount-error').textContent = 'Invalid amount' |
| 3112 | 3171 | hasError = true |
| 3113 | | } else if (Number(amount) > currentBalance) { |
| 3172 | } else if (amountInSmallestUnits > currentBalance) { |
| 3114 | 3173 | document.getElementById('send-amount-error').textContent = 'Insufficient balance' |
| 3115 | 3174 | hasError = true |
| 3116 | 3175 | } |
| … | … | @@ -3130,13 +3189,39 @@ function confirmSend() { |
| 3130 | 3189 | const confirmModal = document.getElementById('confirm-modal') |
| 3131 | 3190 | const confirmContent = document.getElementById('confirm-content') |
| 3132 | 3191 | |
| 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 | |
| 3136 | 3207 | const feeFormatted = currentSendChain === 'bitcoin' |
| 3137 | 3208 | ? formatSats(estimatedFee) |
| 3138 | 3209 | : formatEthFee(estimatedFee) |
| 3139 | 3210 | |
| 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 | |
| 3140 | 3225 | const networkWarning = selectedNetwork === 'mainnet' |
| 3141 | 3226 | ? '<div class="confirm-warning">⚠️ WARNING: You are sending on MAINNET. This will use real funds!</div>' |
| 3142 | 3227 | : '' |
| … | … | @@ -3161,7 +3246,7 @@ function confirmSend() { |
| 3161 | 3246 | </div> |
| 3162 | 3247 | <div class="confirm-row"> |
| 3163 | 3248 | <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> |
| 3165 | 3250 | </div> |
| 3166 | 3251 | ` |
| 3167 | 3252 | |
| … | … | @@ -3176,7 +3261,10 @@ function closeConfirm() { |
| 3176 | 3261 | // Execute send transaction |
| 3177 | 3262 | async function executeSend() { |
| 3178 | 3263 | 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) |
| 3180 | 3268 | |
| 3181 | 3269 | // Close confirmation modal |
| 3182 | 3270 | closeConfirm() |
| … | … | @@ -3210,9 +3298,13 @@ async function executeSend() { |
| 3210 | 3298 | |
| 3211 | 3299 | // Show native notification (don't let this crash the app) |
| 3212 | 3300 | try { |
| 3301 | const isToken = currentSendChain.includes('usdt') || currentSendChain.includes('usdc') |
| 3302 | const displayUnit = isToken |
| 3303 | ? (currentSendChain.includes('usdt') ? 'USDT' : 'USDC') |
| 3304 | : currentSendChain.toUpperCase() |
| 3213 | 3305 | showNotification( |
| 3214 | 3306 | 'Transaction Sent', |
| 3215 | | `${amount} ${currentSendChain.toUpperCase()} sent successfully`, |
| 3307 | `${amountInput} ${displayUnit} sent successfully`, |
| 3216 | 3308 | 'success' |
| 3217 | 3309 | ) |
| 3218 | 3310 | } catch (notifErr) { |