diff --git a/drongo b/drongo index ee732fb2..de87ab11 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit ee732fb2235fbe242d75366fc37d3f53e2082519 +Subproject commit de87ab1102db12cad8bbfe814a1346078cf957a5 diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelsChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelsChangedEvent.java index 91f696f2..ae0f6759 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelsChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelsChangedEvent.java @@ -3,25 +3,37 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.wallet.Wallet; 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. */ public class WalletEntryLabelsChangedEvent extends WalletChangedEvent { - private final List entries; + //Contains the changed entry mapped to the entry that changed it, if changed recursively (otherwise null) + private final Map entrySourceMap; public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) { - super(wallet); - this.entries = List.of(entry); + this(wallet, List.of(entry)); } public WalletEntryLabelsChangedEvent(Wallet wallet, List entries) { super(wallet); - this.entries = entries; + this.entrySourceMap = new LinkedHashMap<>(); + for(Entry entry : entries) { + entrySourceMap.put(entry, null); + } } - public List getEntries() { - return entries; + public WalletEntryLabelsChangedEvent(Wallet wallet, Map entrySourceMap) { + super(wallet); + this.entrySourceMap = entrySourceMap; + } + + public Collection getEntries() { + return entrySourceMap.keySet(); + } + + public Entry getSource(Entry entry) { + return entrySourceMap.get(entry); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index b5745e51..0a7defac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -227,6 +227,8 @@ public class DbPersistence implements Persistence { if(dirtyPersistables.clearHistory) { WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class); + DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class); + detachedLabelDao.clearAndAddAll(wallet); walletNodeDao.clearHistory(wallet); blockTransactionDao.clear(wallet.getId()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelDao.java new file mode 100644 index 00000000..292b53e3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelDao.java @@ -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 getAll(); + + @SqlBatch("insert into detachedLabel (entry, label) values (?, ?)") + void insertDetachedLabels(List entries, List labels); + + @SqlUpdate("delete from detachedLabel") + void clear(); + + default void clearAndAddAll(Wallet wallet) { + clear(); + + List entries = new ArrayList<>(); + List labels = new ArrayList<>(); + for(Map.Entry 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); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelMapper.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelMapper.java new file mode 100644 index 00000000..294b3369 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DetachedLabelMapper.java @@ -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> { + @Override + public Map.Entry 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; + } + }; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java index 938d9e73..07a7c665 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java @@ -30,6 +30,9 @@ public interface WalletDao { @CreateSqlObject BlockTransactionDao createBlockTransactionDao(); + @CreateSqlObject + DetachedLabelDao createDetachedLabelDao(); + @CreateSqlObject MixConfigDao createMixConfigDao(); @@ -100,9 +103,12 @@ public interface WalletDao { List walletNodes = createWalletNodeDao().getForWalletId(wallet.getId()); wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList())); - Map blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new)); + Map blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); wallet.updateTransactions(blockTransactions); + Map detachedLabels = createDetachedLabelDao().getAll(); + wallet.getDetachedLabels().putAll(detachedLabels); + wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId())); Map utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId()); @@ -120,6 +126,7 @@ public interface WalletDao { createKeystoreDao().addKeystores(wallet); createWalletNodeDao().addWalletNodes(wallet); createBlockTransactionDao().addBlockTransactions(wallet); + createDetachedLabelDao().clearAndAddAll(wallet); createMixConfigDao().addMixConfig(wallet); createUtxoMixDataDao().addUtxoMixData(wallet); } finally { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 74de1d02..9bdd8dd4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -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) int gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap); while(historySize < gapLimitSize) { - purposeNode.fillToIndex(gapLimitSize - 1); + purposeNode.fillToIndex(wallet, gapLimitSize - 1); subscribeWalletNodes(wallet, getAddressNodes(wallet, purposeNode), nodeTransactionMap, historySize); getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, historySize); getReferencedTransactions(wallet, nodeTransactionMap); @@ -718,7 +718,7 @@ public class ElectrumServer { } if(!transactionOutputs.equals(node.getTransactionOutputs())) { - node.updateTransactionOutputs(transactionOutputs); + node.updateTransactionOutputs(wallet, transactionOutputs); copyPostmixLabels(wallet, transactionOutputs); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java index 79d37e81..77eb8b9e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java @@ -64,8 +64,8 @@ public class SettingsWalletForm extends WalletForm { AppServices.clearTransactionHistoryCache(wallet); } - //Clear node tree - walletCopy.clearNodes(); + //Clear node tree, detaching and saving any labels from the existing wallet + walletCopy.clearNodes(wallet); Integer childIndex = wallet.isMasterWallet() ? null : wallet.getMasterWallet().getChildWallets().indexOf(wallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 571292c4..3149481b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -472,48 +472,46 @@ public class WalletForm { @Subscribe public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) { if(event.getWallet() == wallet) { - List labelChangedEntries = new ArrayList<>(); + Map labelChangedEntries = new LinkedHashMap<>(); for(Entry entry : event.getEntries()) { if(entry.getLabel() != null && !entry.getLabel().isEmpty()) { - if(entry instanceof TransactionEntry) { - TransactionEntry transactionEntry = (TransactionEntry)entry; + if(entry instanceof TransactionEntry transactionEntry) { for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) { for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) { if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) { if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { 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()); - 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())) { 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) { - NodeEntry nodeEntry = (NodeEntry)entry; + if(entry instanceof NodeEntry nodeEntry) { for(BlockTransactionHashIndex receivedRef : nodeEntry.getNode().getTransactionOutputs()) { BlockTransaction blockTransaction = event.getWallet().getTransactions().get(receivedRef.getHash()); if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) { 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) { - HashIndexEntry hashIndexEntry = (HashIndexEntry)entry; + if(entry instanceof HashIndexEntry hashIndexEntry) { BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction(); if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) { 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 optPurposeNode = wallet.getPurposeNodes().stream().filter(node -> node.getKeyPurpose() == keyPurpose).findFirst(); if(optPurposeNode.isPresent()) { WalletNode purposeNode = optPurposeNode.get(); - newNodes.addAll(purposeNode.fillToIndex(wallet.getLookAheadIndex(purposeNode))); + newNodes.addAll(purposeNode.fillToIndex(wallet, wallet.getLookAheadIndex(purposeNode))); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/sql/V5__DetachedLabel.sql b/src/main/resources/com/sparrowwallet/sparrow/sql/V5__DetachedLabel.sql new file mode 100644 index 00000000..2d624f34 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/sql/V5__DetachedLabel.sql @@ -0,0 +1 @@ +create table detachedLabel (entry varchar(80) primary key not null, label varchar(255) not null); \ No newline at end of file