mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
draft implementation of optional bip329 fields
This commit is contained in:
parent
1140a678ad
commit
20d3f07059
4 changed files with 180 additions and 35 deletions
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue