cut pasted amounts to the unit precision in the send tab and send to many, and stop the csv import skipping fractional sats and exponent amounts
What changed, and why it matters
This commit fixes how Sparrow Wallet handles pasted or imported Bitcoin amounts. Previously, very small or oddly formatted amounts (like scientific notation '1e-8' or fractional satoshis) could be misread or silently skipped during CSV imports. The change now cuts pasted amounts to the correct precision and parses CSV amounts more carefully, preventing accidental wrong payment amounts.
Review the new UnitFormatDoubleConverter for edge cases around zero, negative, and maximum satoshi values. Verify that truncation behavior is consistently applied and does not introduce off-by-one-satoshi errors. Test CSV imports with scientific notation, fractional sats, locale-specific separators, and header rows.
Security signals we found
Amount parsing inconsistency between UI paste and CSV import
Silent swallowing of NumberFormatException could skip payment rows
Use of Double.parseDouble for monetary amounts
Potential truncation vs rounding behavior change in amount handling
BigDecimal introduced to improve precision handling
Evidence from the diff
The patch modifies CoinTextFormatter.java to truncate pasted amounts to the unit’s allowed precision rather than rejecting or ignoring them. In SendToManyDialog.java, CSV amount parsing is refactored to use a shared UnitFormatDoubleConverter that parses input as BigDecimal, handles scientific notation, rejects out-of-range/hex/NaN/Infinity values, and truncates extra precision. Previously, Double.parseDouble was used for BTC and Long.parseLong for sats, with NumberFormatException silently swallowed—potentially skipping rows or misinterpreting fractional sats and exponent notation.
Changed components
src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.javasrc/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.javaInspect captured patch +39 / −36
### src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.java
@@ -59,11 +59,12 @@ public Change apply(Change change) {
boolean validAmount = coinValidation.matcher(noFractionCommaText).matches();
if(!validAmount) {
- //The amount a pasted text starts with is taken, unless it is more precise than the unit allows - that is ignored rather than truncated,
- //so a digit typed beyond the last place leaves the field as it was
+ //The amount a pasted text starts with is taken, cut to the places the unit has. A digit typed beyond the last place is ignored instead,
+ //leaving what has been typed as it was rather than rewriting it
Matcher leadingAmount = anyPrecisionAmount.matcher(noFractionCommaText);
- if(leadingAmount.find() && coinValidation.matcher(leadingAmount.group()).matches()) {
- noFractionCommaText = leadingAmount.group();
+ Matcher amount = coinValidation.matcher(leadingAmount.find() ? leadingAmount.group() : "");
+ if(amount.matches() || (change.getText().length() > 1 && amount.lookingAt())) {
+ noFractionCommaText = amount.group();
} else {
return null;
}
### src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
@@ -37,6 +37,8 @@
import org.controlsfx.control.spreadsheet.*;
import java.io.*;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
@@ -225,34 +227,27 @@ protected Node createButton(ButtonType buttonType) {
}
try {
- String rawAmount = csvReader.get(1).trim();
- String groupingStripped = rawAmount.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
- long amount;
- if(bitcoinUnit == BitcoinUnit.BTC) {
- String normalised = groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
- double doubleAmount = Double.parseDouble(normalised);
- amount = bitcoinUnit.getSatsValue(doubleAmount);
- } else {
- amount = Long.parseLong(groupingStripped);
- }
- String label = csvReader.get(2);
- Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
- if(optDnsPaymentHrn.isPresent()) {
- Payment payment = new Payment(null, label, amount, false);
- csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
- } else {
- try {
- SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
- Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
- csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
- } catch(Exception e) {
- Address address = Address.fromString(csvReader.get(0));
- Payment payment = new Payment(address, label, amount, false);
- csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
+ //Read as a pasted amount is, so digits beyond the places of the unit are cut - a row without an amount is probably a header line
+ Double value = amountCellType.convertValue(csvReader.get(1));
+ if(value != null) {
+ long amount = bitcoinUnit.getSatsValue(value);
+ String label = csvReader.get(2);
+ Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
+ if(optDnsPaymentHrn.isPresent()) {
+ Payment payment = new Payment(null, label, amount, false);
+ csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
+ } else {
+ try {
+ SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
+ Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
+ csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
+ } catch(Exception e) {
+ Address address = Address.fromString(csvReader.get(0));
+ Payment payment = new Payment(address, label, amount, false);
+ csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
+ }
}
}
- } catch(NumberFormatException e) {
- //ignore and continue - probably a header line
} catch(InvalidAddressException e) {
AppServices.showErrorDialog("Invalid Address", e.getMessage());
}
@@ -433,6 +428,9 @@ public String toString(Double item, String format) {
}
private static class UnitFormatDoubleConverter extends StringConverterWithFormat<Double> {
+ //2,100,000,000,000,000 sats is every bitcoin there will be
+ private static final int MAX_SATS_DIGITS = 16;
+
private final UnitFormat unitFormat;
private final BitcoinUnit bitcoinUnit;
@@ -446,15 +444,19 @@ public Double fromString(String str) {
if(str == null || str.isEmpty()) {
return null;
}
- String groupingStripped = str.trim().replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
+ String normalised = str.trim().replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "").replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
try {
- //A sats amount with a fraction is not read, as in a CSV import, rather than truncated when paid - a paste of it clears the cell like any text that is not an amount
- if(bitcoinUnit == BitcoinUnit.SATOSHIS) {
- return (double)Long.parseLong(groupingStripped);
+ //Read as an exact decimal, which takes the exponent a script writes a small amount with, but not the hexadecimal, NaN or Infinity a double parses
+ BigDecimal sats = new BigDecimal(normalised).scaleByPowerOfTen(bitcoinUnit == BitcoinUnit.BTC ? 8 : 0);
+ //Sized by its digits before the point without rescaling it, as rescaling an exponent far out of range takes minutes
+ long wholeDigits = (long)sats.precision() - sats.scale();
+ if(sats.signum() < 0 || wholeDigits > MAX_SATS_DIGITS) {
+ return null;
}
- return Double.valueOf(groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), "."));
- } catch(NumberFormatException e) {
+ //Digits beyond the places of the unit are cut rather than rounded, so the cell shows the amount of the payment
+ return bitcoinUnit.getValue(wholeDigits < 1 ? 0 : sats.setScale(0, RoundingMode.DOWN).longValueExact());
+ } catch(NumberFormatException | ArithmeticException e) {
return null;
}
}Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.