merge wallet labels optional fields draft implementation

This commit is contained in:
Craig Raw 2025-02-26 10:47:17 +02:00
commit 76eff2de48
4 changed files with 180 additions and 35 deletions

View file

@ -1369,7 +1369,7 @@ public class AppController implements Initializable {
public void exportWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
dlg.initOwner(rootStack.getScene().getWindow());
Optional<Wallet> wallet = dlg.showAndWait();
if(wallet.isPresent()) {

View file

@ -18,8 +18,8 @@ import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> {
private Wallet wallet;
public WalletExportDialog(WalletForm walletForm) {
this.wallet = walletForm.getWallet();
public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
this.wallet = selectedWalletForm.getWallet();
EventManager.get().register(this);
setOnCloseRequest(event -> {
@ -45,10 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}

View file

@ -1,33 +1,40 @@
package com.sparrowwallet.sparrow.io;
import com.csvreader.CsvReader;
import com.google.gson.Gson;
import com.google.gson.*;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.*;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WalletLabels implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
private static final long ONE_DAY = 24*60*60*1000L;
private final List<WalletForm> walletForms;
public WalletLabels() {
this.walletForms = Collections.emptyList();
}
public WalletLabels(List<WalletForm> walletForms) {
this.walletForms = walletForms;
}
@ -50,8 +57,9 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
for(Wallet exportWallet : allWallets) {
Map<Date, Double> fiatRates = getFiatRates(walletForms);
for(WalletForm exportWalletForm : walletForms) {
Wallet exportWallet = exportWalletForm.getWallet();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
String origin = outputDescriptor.toString(true, false, false);
@ -61,34 +69,38 @@ public class WalletLabels implements WalletImport, WalletExport {
}
}
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null));
}
WalletTransactionsEntry walletTransactionsEntry = exportWalletForm.getWalletTransactionsEntry();
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
BlockTransaction blkTx = txEntry.getBlockTransaction();
labels.add(new TransactionLabel(blkTx.getHashAsString(), blkTx.getLabel(), origin,
txEntry.isConfirming() ? null : blkTx.getHeight(), blkTx.getDate(),
blkTx.getFee() == null || blkTx.getFee() == 0 ? null : blkTx.getFee(), txEntry.getValue(),
getFiatValue(blkTx.getDate(), Transaction.SATOSHIS_PER_BITCOIN, fiatRates)));
}
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null));
}
labels.add(new AddressLabel(addressNode.getAddress().toString(), addressNode.getLabel(), origin, addressNode.getDerivationPath().substring(1),
addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)).map(BlockTransactionHash::getHeight).toList()));
}
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true");
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable));
} else if(!txo.isSpent()) {
labels.add(new Label(Type.output, txo.toString(), null, null, spendable));
}
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : exportWallet.getWalletTxos().entrySet()) {
BlockTransactionHashIndex txo = txoEntry.getKey();
WalletNode addressNode = txoEntry.getValue();
Boolean spendable = (txo.isSpent() ? null : txo.getStatus() != Status.FROZEN);
labels.add(new InputOutputLabel(Type.output, txo.toString(), txo.getLabel(), origin, spendable, addressNode.getDerivationPath().substring(1),
txo.getValue(), txo.getHeight(), txo.getDate(), getFiatValue(txo, fiatRates)));
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null));
if(txo.isSpent()) {
BlockTransactionHashIndex txi = txo.getSpentBy();
labels.add(new InputOutputLabel(Type.input, txi.toString(), txi.getLabel(), origin, null, addressNode.getDerivationPath().substring(1),
txi.getValue(), null, null, getFiatValue(txi, fiatRates)));
}
}
}
try {
Gson gson = new Gson();
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new GsonUTCDateAdapter()).create();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for(Label label : labels) {
@ -247,11 +259,11 @@ public class WalletLabels implements WalletImport, WalletExport {
addChangedEntry(changedWalletEntries, txioEntry);
}
if(label.type == Type.output && !reference.isSpent()) {
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) {
if(label.type == Type.output && !reference.isSpent() && label.spendable != null) {
if(!label.spendable && reference.getStatus() != Status.FROZEN) {
reference.setStatus(Status.FROZEN);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) {
} else if(label.spendable && reference.getStatus() == Status.FROZEN) {
reference.setStatus(null);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
}
@ -324,12 +336,77 @@ public class WalletLabels implements WalletImport, WalletExport {
return true;
}
private Map<Date, Double> getFiatRates(List<WalletForm> walletForms) {
ExchangeSource exchangeSource = getExchangeSource();
Currency fiatCurrency = getFiatCurrency();
Map<Date, Double> fiatRates = new HashMap<>();
if(fiatCurrency != null) {
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
for(WalletForm walletForm : walletForms) {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
if(!walletTransactionsEntry.getChildren().isEmpty()) {
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
.filter(Objects::nonNull)
.collect(Collectors.summarizingLong(Date::getTime));
min = Math.min(min, stats.getMin());
max = Math.max(max, stats.getMax());
}
}
if(max > min) {
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(min - ONE_DAY), new Date(max));
}
}
return fiatRates;
}
private static ExchangeSource getExchangeSource() {
return Config.get().getExchangeSource() == null ? ExchangeSource.COINGECKO : Config.get().getExchangeSource();
}
private static Currency getFiatCurrency() {
return getExchangeSource() == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency();
}
private Map<Currency, BigDecimal> getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
return getFiatValue(txEntry.getBlockTransaction().getDate(), txEntry.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(BlockTransactionHashIndex ref, Map<Date, Double> fiatRates) {
return getFiatValue(ref.getDate(), ref.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(Date date, long value, Map<Date, Double> fiatRates) {
Currency fiatCurrency = getFiatCurrency();
if(fiatCurrency != null) {
Double dayRate = null;
if(date == null) {
if(AppServices.getFiatCurrencyExchangeRate() != null) {
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
}
} else {
dayRate = fiatRates.get(DateUtils.truncate(date, Calendar.DAY_OF_MONTH));
}
if(dayRate != null) {
BigDecimal fiatValue = BigDecimal.valueOf(dayRate * value / Transaction.SATOSHIS_PER_BITCOIN);
return Map.of(fiatCurrency, fiatValue.setScale(fiatCurrency.getDefaultFractionDigits(), RoundingMode.HALF_UP));
}
}
return null;
}
private enum Type {
tx, addr, pubkey, input, output, xpub
}
private static class Label {
public Label(Type type, String ref, String label, String origin, String spendable) {
public Label(Type type, String ref, String label, String origin, Boolean spendable) {
this.type = type;
this.ref = ref;
this.label = label;
@ -341,6 +418,74 @@ public class WalletLabels implements WalletImport, WalletExport {
String ref;
String label;
String origin;
String spendable;
Boolean spendable;
}
private static class TransactionLabel extends Label {
public TransactionLabel(String ref, String label, String origin, Integer height, Date time, Long fee, Long value, Map<Currency, BigDecimal> rate) {
super(Type.tx, ref, label, origin, null);
this.height = height;
this.time = time;
this.fee = fee;
this.value = value;
this.rate = rate;
}
Integer height;
Date time;
Long fee;
Long value;
Map<Currency, BigDecimal> rate;
}
private static class AddressLabel extends Label {
public AddressLabel(String ref, String label, String origin, String keypath, List<Integer> heights) {
super(Type.addr, ref, label, origin, null);
this.keypath = keypath;
this.heights = heights;
}
String keypath;
List<Integer> heights;
}
private static class InputOutputLabel extends Label {
public InputOutputLabel(Type type, String ref, String label, String origin, Boolean spendable, String keypath, Long value, Integer height, Date time, Map<Currency, BigDecimal> fmv) {
super(type, ref, label, origin, spendable);
this.keypath = keypath;
this.value = value;
this.height = height;
this.time = time;
this.fmv = fmv;
}
String keypath;
Long value;
Integer height;
Date time;
Map<Currency, BigDecimal> fmv;
}
public static class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
private final DateFormat dateFormat;
public GsonUTCDateAdapter() {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
public JsonElement serialize(Date src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(dateFormat.format(src));
}
@Override
public Date deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return dateFormat.parse(json.getAsString());
} catch (ParseException e) {
throw new JsonParseException(e);
}
}
}
}

View file

@ -591,7 +591,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
if(walletForm instanceof SettingsWalletForm settingsWalletForm) {
WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm());
WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm(), List.of(settingsWalletForm.getAppWalletForm()));
dlg.initOwner(export.getScene().getWindow());
dlg.showAndWait();
} else {