update related address, tx, txi and txo labels when null, set tx, txi, txo labels to address label on receive, add paste label to context menu

This commit is contained in:
Craig Raw 2021-05-14 18:03:46 +02:00
parent fb72010bdf
commit d67c5c5218
14 changed files with 188 additions and 54 deletions

View file

@ -104,22 +104,21 @@ public class AddressTreeTable extends CoinTreeTable {
}
public void updateHistory(List<WalletNode> updatedNodes) {
//We only ever add or replace child nodes - never remove in order to keep a full sequence
//We only ever add child nodes - never remove in order to keep a full sequence
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
for(WalletNode updatedNode : updatedNodes) {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
if(optEntry.isPresent()) {
int index = rootEntry.getChildren().indexOf(optEntry.get());
rootEntry.getChildren().set(index, nodeEntry);
NodeEntry existingEntry = (NodeEntry)optEntry.get();
existingEntry.refreshChildren();
} else {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
rootEntry.getChildren().add(nodeEntry);
}
}
sort();
refresh();
}
public void updateLabel(Entry entry) {

View file

@ -6,6 +6,7 @@ import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.util.converter.DefaultStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,10 +29,11 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
setText(label);
setContextMenu(new LabelContextMenu(label));
setContextMenu(new LabelContextMenu(entry, label));
}
}
@ -80,7 +82,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
@ -88,8 +90,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyLabel);
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(content instanceof String) {
MenuItem pasteLabel = new MenuItem("Paste Label");
pasteLabel.setOnAction(AE -> {
hide();
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(currentContent instanceof String) {
entry.labelProperty().set((String)currentContent);
}
});
getItems().add(pasteLabel);
}
}
}
}

View file

@ -1,21 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
/**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
* Extends WalletDataChangedEvent so triggers a background save.
*/
public class WalletEntryLabelChangedEvent extends WalletDataChangedEvent {
private final Entry entry;
public WalletEntryLabelChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
}

View file

@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
import java.util.List;
/**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
* Extends WalletDataChangedEvent so triggers a background save.
*/
public class WalletEntryLabelsChangedEvent extends WalletDataChangedEvent {
private final List<Entry> entries;
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entries = List.of(entry);
}
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
super(wallet);
this.entries = entries;
}
public List<Entry> getEntries() {
return entries;
}
}

View file

@ -76,7 +76,7 @@ public class JsonPersistence implements Persistence {
Map<File, Wallet> loadedWallets = loadWallets(walletFiles, encryptionKey);
for(Map.Entry<File, Wallet> entry : loadedWallets.entrySet()) {
Storage storage = new Storage(entry.getKey());
storage.setEncryptionPubKey(ECKey.fromPublicOnly(encryptionKey));
storage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
storage.setKeyDeriver(getKeyDeriver());
Wallet childWallet = entry.getValue();
childWallet.setMasterWallet(masterWallet);

View file

@ -20,6 +20,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.payjoin.Payjoin;
import com.sparrowwallet.sparrow.payjoin.PayjoinReceiverException;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.application.Platform;
@ -1156,24 +1157,29 @@ public class HeadersController extends TransactionFormController implements Init
if(headersForm.getSigningWallet() != null && !(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet)) {
Sha256Hash txid = headersForm.getTransaction().getTxId();
List<Entry> changedLabelEntries = new ArrayList<>();
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid);
if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel(headersForm.getName());
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()))));
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
}
for(WalletNode walletNode : event.getHistoryChangedNodes()) {
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
output.setLabel(headersForm.getName() + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()))));
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
}
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
output.getSpentBy().setLabel(headersForm.getName() + " (input)");
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output.getSpentBy(), HashIndexEntry.Type.INPUT, walletNode.getKeyPurpose()))));
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output.getSpentBy(), HashIndexEntry.Type.INPUT, walletNode.getKeyPurpose()));
}
}
}
if(!changedLabelEntries.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
}
}
}

View file

@ -69,10 +69,12 @@ public class AddressesController extends WalletFormController implements Initial
}
@Subscribe
public void walletEntryLabelChanged(WalletEntryLabelChangedEvent event) {
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
receiveTable.updateLabel(event.getEntry());
changeTable.updateLabel(event.getEntry());
for(Entry entry : event.getEntries()) {
receiveTable.updateLabel(entry);
changeTable.updateLabel(entry);
}
}
}

View file

@ -7,7 +7,7 @@ import com.sparrowwallet.drongo.wallet.Status;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.DateLabel;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import java.util.Collections;
@ -27,7 +27,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
labelProperty().addListener((observable, oldValue, newValue) -> {
hashIndex.setLabel(newValue);
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
});
}

View file

@ -5,25 +5,23 @@ import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class NodeEntry extends Entry implements Comparable<NodeEntry> {
private final WalletNode node;
public NodeEntry(Wallet wallet, WalletNode node) {
super(wallet, node.getLabel(),
!node.getChildren().isEmpty() ?
node.getChildren().stream().filter(childNode -> !Config.get().isHideEmptyUsedAddresses() || childNode.getTransactionOutputs().isEmpty() || !childNode.getUnspentTransactionOutputs().isEmpty()).map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()) :
node.getTransactionOutputs().stream().map(txo -> new HashIndexEntry(wallet, txo, HashIndexEntry.Type.OUTPUT, node.getKeyPurpose())).collect(Collectors.toList()));
super(wallet, node.getLabel(), createChildren(wallet, node));
this.node = node;
labelProperty().addListener((observable, oldValue, newValue) -> {
node.setLabel(newValue);
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
});
}
@ -43,6 +41,17 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
return getWallet().getOutputDescriptor(node);
}
public void refreshChildren() {
getChildren().clear();
getChildren().addAll(createChildren(getWallet(), node));
}
private static List<Entry> createChildren(Wallet wallet, WalletNode node) {
return !node.getChildren().isEmpty() ?
node.getChildren().stream().filter(childNode -> !Config.get().isHideEmptyUsedAddresses() || childNode.getTransactionOutputs().isEmpty() || !childNode.getUnspentTransactionOutputs().isEmpty()).map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()) :
node.getTransactionOutputs().stream().map(txo -> new HashIndexEntry(wallet, txo, HashIndexEntry.Type.OUTPUT, node.getKeyPurpose())).collect(Collectors.toList());
}
@Override
public Long getValue() {
if(node.getTransactionOutputs().isEmpty()) {
@ -52,6 +61,19 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
return node.getUnspentValue();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NodeEntry that = (NodeEntry) o;
return getWallet().equals(that.getWallet()) && node.equals(that.node);
}
@Override
public int hashCode() {
return Objects.hash(getWallet(), node);
}
@Override
public int compareTo(NodeEntry other) {
return node.compareTo(other.node);

View file

@ -1026,7 +1026,7 @@ public class SendController extends WalletFormController implements Initializabl
}
@Subscribe
public void walletEntryLabelChanged(WalletEntryLabelChangedEvent event) {
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
updateTransaction();
}

View file

@ -8,7 +8,7 @@ import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
@ -31,7 +31,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
labelProperty().addListener((observable, oldValue, newValue) -> {
blockTransaction.setLabel(newValue);
EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this));
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
});
setConfirmations(calculateConfirmations());

View file

@ -197,9 +197,11 @@ public class TransactionsController extends WalletFormController implements Init
}
@Subscribe
public void walletEntryLabelChanged(WalletEntryLabelChangedEvent event) {
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
transactionsTable.updateLabel(event.getEntry());
for(Entry entry : event.getEntries()) {
transactionsTable.updateLabel(entry);
}
balanceChart.update(getWalletForm().getWalletTransactionsEntry());
}
}

View file

@ -114,9 +114,11 @@ public class UtxosController extends WalletFormController implements Initializab
}
@Subscribe
public void walletEntryLabelChanged(WalletEntryLabelChangedEvent event) {
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
utxosTable.updateLabel(event.getEntry());
for(Entry entry : event.getEntries()) {
utxosTable.updateLabel(entry);
}
utxosChart.update(getWalletForm().getWalletUtxosEntry());
}
}

View file

@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
@ -314,6 +315,85 @@ public class WalletForm {
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWalletFile().equals(storage.getWalletFile())) {
for(WalletNode changedNode : event.getHistoryChangedNodes()) {
if(changedNode.getLabel() != null && !changedNode.getLabel().isEmpty()) {
List<Entry> changedLabelEntries = new ArrayList<>();
for(BlockTransactionHashIndex receivedRef : changedNode.getTransactionOutputs()) {
BlockTransaction blockTransaction = wallet.getTransactions().get(receivedRef.getHash());
if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel(changedNode.getLabel());
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
}
}
if(!changedLabelEntries.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
}
}
}
}
}
@Subscribe
public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet() == wallet) {
List<Entry> labelChangedEntries = new ArrayList<>();
for(Entry entry : event.getEntries()) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : keyPurposes) {
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
if(receivedRef.getLabel() == null) {
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose));
}
if(childNode.getLabel() == null) {
childNode.setLabel(entry.getLabel());
labelChangedEntries.add(new NodeEntry(event.getWallet(), childNode));
}
}
if(receivedRef.isSpent() && receivedRef.getSpentBy().getHash().equals(transactionEntry.getBlockTransaction().getHash()) && receivedRef.getSpentBy().getLabel() == null) {
receivedRef.getSpentBy().setLabel(entry.getLabel() + " (input)");
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef.getSpentBy(), HashIndexEntry.Type.INPUT, keyPurpose));
}
}
}
}
}
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
for(BlockTransactionHashIndex receivedRef : nodeEntry.getNode().getTransactionOutputs()) {
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(receivedRef.getHash());
if(blockTransaction.getLabel() == null) {
blockTransaction.setLabel(entry.getLabel());
labelChangedEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
}
}
}
if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction();
if(blockTransaction.getLabel() == null) {
blockTransaction.setLabel(entry.getLabel());
labelChangedEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
}
}
}
}
if(!labelChangedEntries.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, labelChangedEntries)));
}
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {