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

This commit is contained in:
Craig Raw
2026-09-17 09:10:26 +02:00
parent a573f22aef
commit 9e999d3f07
2 changed files with 39 additions and 36 deletions
@@ -59,11 +59,12 @@ public class CoinTextFormatter extends TextFormatter<String> {
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;
}
@@ -37,6 +37,8 @@ import javafx.util.StringConverter;
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 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
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 class SendToManyDialog extends Dialog<List<Payment>> {
}
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 class SendToManyDialog extends Dialog<List<Payment>> {
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;
}
}