diff --git a/CHANGELOG.md b/CHANGELOG.md index 405855e..b809598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to Q-Wallets will be documented in this file. +## [1.3.3] + +### Added + +- `calculateMaxSendable` utility that computes the "SEND MAX" amount in integer satoshi math and holds back a small safety buffer, avoiding floating-point boundary errors that triggered false "Insufficient funds" rejections from the host +- `SEND_MAX_SAFETY_BUFFER_SATS` constant (1000 sats) used as the SEND MAX safety margin + +### Fixed + +- BTC and DGB Bech32 address validation: corrected the regex charset to the bech32 alphabet (excludes `1`, `b`, `i`, `o`) and allowed variable address length (39–59 chars) so longer Bech32 addresses (e.g. P2WSH) validate correctly +- SEND MAX no longer prefills an amount that lands on or just above the spendable cutoff + +### Changed + +- All coin send pages (ARRR, BTC, DGB, DOGE, LTC, QORT, RVN) refactored to use the shared `calculateMaxSendable` utility + +### Tests + +- Added tests for `calculateMaxSendable` +- Expanded address validation tests covering BTC/DGB Bech32 charset and length cases + ## [1.3.2] - 2026-03-06 ### Fixed diff --git a/package-lock.json b/package-lock.json index 33e3789..b8aeebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "q-wallets", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "q-wallets", - "version": "1.3.2", + "version": "1.3.3", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index 07e60c0..2c652f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "q-wallets", "private": true, - "version": "1.3.2", + "version": "1.3.3", "type": "module", "scripts": { "build": "tsc -b && vite build && cp CHANGELOG.md dist/", diff --git a/src/common/constants.ts b/src/common/constants.ts index bc9d706..e37fe88 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -11,6 +11,12 @@ export const DECIMAL_ROUND_UP: number = 8; // QORT section export const QORT_1_UNIT: number = 100000000; +// Satoshis held back by "SEND MAX" so the prefilled amount stays strictly +// within the spendable balance (absorbs fee-estimate / serialization slack). +// 1000 sats = 0.00001 coin — negligible to the user, enough to clear the +// host's "Insufficient funds" boundary check. +export const SEND_MAX_SAFETY_BUFFER_SATS: number = 1000; + // Fees export const ARRR_FEE: number = 0.0001; export const BTC_FEE: number = 500; diff --git a/src/pages/arrr/index.tsx b/src/pages/arrr/index.tsx index a6305c2..28107d1 100644 --- a/src/pages/arrr/index.tsx +++ b/src/pages/arrr/index.tsx @@ -67,6 +67,7 @@ import { ARRR_FEE, DECIMAL_ROUND_UP, EMPTY_STRING, + SEND_MAX_SAFETY_BUFFER_SATS, TIME_MINUTES_2, TIME_MINUTES_3, TIME_MINUTES_5, @@ -88,6 +89,7 @@ import { } from '../../styles/page-styles'; import { Coin } from 'qapp-core'; import { validateArrrAddress } from '../../utils/addressValidation'; +import { calculateMaxSendable } from '../../utils/maxSendable'; interface TablePaginationActionsProps { count: number; @@ -204,18 +206,14 @@ export default function PirateWallet() { const [openArrrAddressBook, setOpenArrrAddressBook] = useState(false); const [_retry, setRetry] = useState(false); - const maxSendableArrrCoin = () => { - // manage the correct round up - const value = (walletBalanceArrr - ARRR_FEE).toString(); - const [integer, decimal = ''] = value.split('.'); - const truncated = decimal - .substring(0, DECIMAL_ROUND_UP) - .padEnd(DECIMAL_ROUND_UP, '0'); - let truncatedMaxSendableArrrCoin: number = parseFloat( - `${integer}.${truncated}` + // Safely-spendable max: integer-satoshi math plus a small safety buffer so + // the prefilled amount never lands on/above the host's spendable cutoff. + const maxSendableArrrCoin = () => + calculateMaxSendable( + walletBalanceArrr, + ARRR_FEE, + SEND_MAX_SAFETY_BUFFER_SATS ); - return truncatedMaxSendableArrrCoin; - }; const emptyRows = page > 0 @@ -258,7 +256,10 @@ export default function PirateWallet() { }; const disableCanSendArrr = () => - arrrAmount <= 0 || arrrRecipient === EMPTY_STRING || addressFormatError; + arrrAmount <= 0 || + arrrRecipient === EMPTY_STRING || + addressFormatError || + arrrAmount > maxSendableArrrCoin(); const handleRecipientChange = (e: ChangeEvent) => { const value = e.target.value.trim(); @@ -329,6 +330,12 @@ export default function PirateWallet() { }; const sendArrrRequest = async () => { + // Conservative revalidation: never submit more than the safely-spendable + // max, even if state changed (e.g. balance refresh) after input. + if (arrrAmount <= 0 || arrrAmount > maxSendableArrrCoin()) { + setOpenSendArrrError(true); + return; + } setOpenTxArrrSubmit(true); try { const sendRequest = await qortalRequest({ @@ -1158,7 +1165,7 @@ export default function PirateWallet() { align="center" sx={{ color: 'text.primary', fontWeight: 700 }} > - {(walletBalanceArrr - ARRR_FEE).toFixed(DECIMAL_ROUND_UP) + ' ARRR'} + {maxSendableArrrCoin() + ' ARRR'}