detach and store labels before a wallet refresh, and label matching entries from this store as the wallet is updated

This commit is contained in:
Craig Raw 2022-02-09 11:44:38 +02:00
parent 4e4fd7501c
commit 7aeca7ebd3
10 changed files with 118 additions and 27 deletions

2
drongo

@ -1 +1 @@
Subproject commit ee732fb2235fbe242d75366fc37d3f53e2082519 Subproject commit de87ab1102db12cad8bbfe814a1346078cf957a5

View file

@ -3,25 +3,37 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.Entry;
import java.util.List; import java.util.*;
/** /**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed. * This event is fired when a wallet entry (transaction, txi or txo) label is changed.
*/ */
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent { public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
private final List<Entry> entries; //Contains the changed entry mapped to the entry that changed it, if changed recursively (otherwise null)
private final Map<Entry, Entry> entrySourceMap;
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) { public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
super(wallet); this(wallet, List.of(entry));
this.entries = List.of(entry);
} }
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) { public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
super(wallet); super(wallet);
this.entries = entries; this.entrySourceMap = new LinkedHashMap<>();
for(Entry entry : entries) {
entrySourceMap.put(entry, null);
}
} }
public List<Entry> getEntries() { public WalletEntryLabelsChangedEvent(Wallet wallet, Map<Entry, Entry> entrySourceMap) {
return entries; super(wallet);
this.entrySourceMap = entrySourceMap;
}
public Collection<Entry> getEntries() {
return entrySourceMap.keySet();
}
public Entry getSource(Entry entry) {
return entrySourceMap.get(entry);
} }
} }

View file

@ -227,6 +227,8 @@ public class DbPersistence implements Persistence {
if(dirtyPersistables.clearHistory) { if(dirtyPersistables.clearHistory) {
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class); BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class);
detachedLabelDao.clearAndAddAll(wallet);
walletNodeDao.clearHistory(wallet); walletNodeDao.clearHistory(wallet);
blockTransactionDao.clear(wallet.getId()); blockTransactionDao.clear(wallet.getId());
} }

View file

@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.*;
public interface DetachedLabelDao {
@SqlQuery("select entry, label from detachedLabel")
@RegisterRowMapper(DetachedLabelMapper.class)
Map<String, String> getAll();
@SqlBatch("insert into detachedLabel (entry, label) values (?, ?)")
void insertDetachedLabels(List<String> entries, List<String> labels);
@SqlUpdate("delete from detachedLabel")
void clear();
default void clearAndAddAll(Wallet wallet) {
clear();
List<String> entries = new ArrayList<>();
List<String> labels = new ArrayList<>();
for(Map.Entry<String, String> labelEntry : new HashSet<>(wallet.getDetachedLabels().entrySet())) {
entries.add(truncate(labelEntry.getKey(), 80));
labels.add(truncate(labelEntry.getValue(), 255));
}
insertDetachedLabels(entries, labels);
}
default String truncate(String label, int length) {
return (label != null && label.length() > length ? label.substring(0, length) : label);
}
}

View file

@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.io.db;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class DetachedLabelMapper implements RowMapper<Map.Entry<String, String>> {
@Override
public Map.Entry<String, String> map(ResultSet rs, StatementContext ctx) throws SQLException {
String entry = rs.getString("entry");
String label = rs.getString("label");
return new Map.Entry<>() {
@Override
public String getKey() {
return entry;
}
@Override
public String getValue() {
return label;
}
@Override
public String setValue(String value) {
return null;
}
};
}
}

View file

@ -30,6 +30,9 @@ public interface WalletDao {
@CreateSqlObject @CreateSqlObject
BlockTransactionDao createBlockTransactionDao(); BlockTransactionDao createBlockTransactionDao();
@CreateSqlObject
DetachedLabelDao createDetachedLabelDao();
@CreateSqlObject @CreateSqlObject
MixConfigDao createMixConfigDao(); MixConfigDao createMixConfigDao();
@ -100,9 +103,12 @@ public interface WalletDao {
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId()); List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId());
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList())); wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new)); Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
wallet.updateTransactions(blockTransactions); wallet.updateTransactions(blockTransactions);
Map<String, String> detachedLabels = createDetachedLabelDao().getAll();
wallet.getDetachedLabels().putAll(detachedLabels);
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId())); wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId()); Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
@ -120,6 +126,7 @@ public interface WalletDao {
createKeystoreDao().addKeystores(wallet); createKeystoreDao().addKeystores(wallet);
createWalletNodeDao().addWalletNodes(wallet); createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet); createBlockTransactionDao().addBlockTransactions(wallet);
createDetachedLabelDao().clearAndAddAll(wallet);
createMixConfigDao().addMixConfig(wallet); createMixConfigDao().addMixConfig(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet); createUtxoMixDataDao().addUtxoMixData(wallet);
} finally { } finally {

View file

@ -320,7 +320,7 @@ public class ElectrumServer {
//The gap limit size takes the highest used index in the retrieved history and adds the gap limit (plus one to be comparable to the number of children since index is zero based) //The gap limit size takes the highest used index in the retrieved history and adds the gap limit (plus one to be comparable to the number of children since index is zero based)
int gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap); int gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap);
while(historySize < gapLimitSize) { while(historySize < gapLimitSize) {
purposeNode.fillToIndex(gapLimitSize - 1); purposeNode.fillToIndex(wallet, gapLimitSize - 1);
subscribeWalletNodes(wallet, getAddressNodes(wallet, purposeNode), nodeTransactionMap, historySize); subscribeWalletNodes(wallet, getAddressNodes(wallet, purposeNode), nodeTransactionMap, historySize);
getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, historySize); getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, historySize);
getReferencedTransactions(wallet, nodeTransactionMap); getReferencedTransactions(wallet, nodeTransactionMap);
@ -718,7 +718,7 @@ public class ElectrumServer {
} }
if(!transactionOutputs.equals(node.getTransactionOutputs())) { if(!transactionOutputs.equals(node.getTransactionOutputs())) {
node.updateTransactionOutputs(transactionOutputs); node.updateTransactionOutputs(wallet, transactionOutputs);
copyPostmixLabels(wallet, transactionOutputs); copyPostmixLabels(wallet, transactionOutputs);
} }
} }

View file

@ -64,8 +64,8 @@ public class SettingsWalletForm extends WalletForm {
AppServices.clearTransactionHistoryCache(wallet); AppServices.clearTransactionHistoryCache(wallet);
} }
//Clear node tree //Clear node tree, detaching and saving any labels from the existing wallet
walletCopy.clearNodes(); walletCopy.clearNodes(wallet);
Integer childIndex = wallet.isMasterWallet() ? null : wallet.getMasterWallet().getChildWallets().indexOf(wallet); Integer childIndex = wallet.isMasterWallet() ? null : wallet.getMasterWallet().getChildWallets().indexOf(wallet);

View file

@ -472,48 +472,46 @@ public class WalletForm {
@Subscribe @Subscribe
public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) { public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet() == wallet) { if(event.getWallet() == wallet) {
List<Entry> labelChangedEntries = new ArrayList<>(); Map<Entry, Entry> labelChangedEntries = new LinkedHashMap<>();
for(Entry entry : event.getEntries()) { for(Entry entry : event.getEntries()) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) { if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
if(entry instanceof TransactionEntry) { if(entry instanceof TransactionEntry transactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) { for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) { for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) {
if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) { if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) {
if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) {
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)")); receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose)); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry);
} }
if(childNode.getLabel() == null || childNode.getLabel().isEmpty()) { //Avoid recursive changes to address labels - only initial transaction label changes can change address labels
if((childNode.getLabel() == null || childNode.getLabel().isEmpty()) && event.getSource(entry) == null) {
childNode.setLabel(entry.getLabel()); childNode.setLabel(entry.getLabel());
labelChangedEntries.add(new NodeEntry(event.getWallet(), childNode)); labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry);
} }
} }
if(receivedRef.isSpent() && receivedRef.getSpentBy().getHash().equals(transactionEntry.getBlockTransaction().getHash()) && (receivedRef.getSpentBy().getLabel() == null || receivedRef.getSpentBy().getLabel().isEmpty())) { if(receivedRef.isSpent() && receivedRef.getSpentBy().getHash().equals(transactionEntry.getBlockTransaction().getHash()) && (receivedRef.getSpentBy().getLabel() == null || receivedRef.getSpentBy().getLabel().isEmpty())) {
receivedRef.getSpentBy().setLabel(entry.getLabel() + " (input)"); receivedRef.getSpentBy().setLabel(entry.getLabel() + " (input)");
labelChangedEntries.add(new HashIndexEntry(event.getWallet(), receivedRef.getSpentBy(), HashIndexEntry.Type.INPUT, keyPurpose)); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef.getSpentBy(), HashIndexEntry.Type.INPUT, keyPurpose), entry);
} }
} }
} }
} }
} }
if(entry instanceof NodeEntry) { if(entry instanceof NodeEntry nodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
for(BlockTransactionHashIndex receivedRef : nodeEntry.getNode().getTransactionOutputs()) { for(BlockTransactionHashIndex receivedRef : nodeEntry.getNode().getTransactionOutputs()) {
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(receivedRef.getHash()); BlockTransaction blockTransaction = event.getWallet().getTransactions().get(receivedRef.getHash());
if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) { if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) {
blockTransaction.setLabel(entry.getLabel()); blockTransaction.setLabel(entry.getLabel());
labelChangedEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry);
} }
} }
} }
if(entry instanceof HashIndexEntry) { if(entry instanceof HashIndexEntry hashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction(); BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction();
if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) { if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) {
blockTransaction.setLabel(entry.getLabel()); blockTransaction.setLabel(entry.getLabel());
labelChangedEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry);
} }
} }
} }
@ -573,7 +571,7 @@ public class WalletForm {
Optional<WalletNode> optPurposeNode = wallet.getPurposeNodes().stream().filter(node -> node.getKeyPurpose() == keyPurpose).findFirst(); Optional<WalletNode> optPurposeNode = wallet.getPurposeNodes().stream().filter(node -> node.getKeyPurpose() == keyPurpose).findFirst();
if(optPurposeNode.isPresent()) { if(optPurposeNode.isPresent()) {
WalletNode purposeNode = optPurposeNode.get(); WalletNode purposeNode = optPurposeNode.get();
newNodes.addAll(purposeNode.fillToIndex(wallet.getLookAheadIndex(purposeNode))); newNodes.addAll(purposeNode.fillToIndex(wallet, wallet.getLookAheadIndex(purposeNode)));
} }
} }

View file

@ -0,0 +1 @@
create table detachedLabel (entry varchar(80) primary key not null, label varchar(255) not null);