mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
optimize and reduce electrum server rpc calls #2
This commit is contained in:
parent
e3138f3392
commit
c77f52f7f6
4 changed files with 135 additions and 57 deletions
|
|
@ -37,6 +37,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class ElectrumServer {
|
public class ElectrumServer {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ElectrumServer.class);
|
private static final Logger log = LoggerFactory.getLogger(ElectrumServer.class);
|
||||||
|
|
@ -224,6 +225,11 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getScriptHashStatus(String scriptHash, WalletNode walletNode) {
|
private static String getScriptHashStatus(String scriptHash, WalletNode walletNode) {
|
||||||
|
List<ScriptHashTx> scriptHashTxes = getScriptHashes(scriptHash, walletNode);
|
||||||
|
return getScriptHashStatus(scriptHashTxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ScriptHashTx> getScriptHashes(String scriptHash, WalletNode walletNode) {
|
||||||
List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs());
|
List<BlockTransactionHashIndex> txos = new ArrayList<>(walletNode.getTransactionOutputs());
|
||||||
txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList()));
|
txos.addAll(walletNode.getTransactionOutputs().stream().filter(BlockTransactionHashIndex::isSpent).map(BlockTransactionHashIndex::getSpentBy).collect(Collectors.toList()));
|
||||||
Set<Sha256Hash> unique = new HashSet<>(txos.size());
|
Set<Sha256Hash> unique = new HashSet<>(txos.size());
|
||||||
|
|
@ -246,10 +252,15 @@ public class ElectrumServer {
|
||||||
sameHeightTxioScriptHashes.add(scriptHash);
|
sameHeightTxioScriptHashes.add(scriptHash);
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
if(!txos.isEmpty()) {
|
|
||||||
|
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getScriptHashStatus(List<ScriptHashTx> scriptHashTxes) {
|
||||||
|
if(!scriptHashTxes.isEmpty()) {
|
||||||
StringBuilder scriptHashStatus = new StringBuilder();
|
StringBuilder scriptHashStatus = new StringBuilder();
|
||||||
for(BlockTransactionHashIndex txo : txos) {
|
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
|
||||||
scriptHashStatus.append(txo.getHash().toString()).append(":").append(txo.getHeight()).append(":");
|
scriptHashStatus.append(scriptHashTx.tx_hash).append(":").append(scriptHashTx.height).append(":");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8)));
|
return Utils.bytesToHex(Sha256Hash.hash(scriptHashStatus.toString().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
|
@ -393,10 +404,12 @@ public class ElectrumServer {
|
||||||
|
|
||||||
public void getReferences(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
|
public void getReferences(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
|
||||||
try {
|
try {
|
||||||
|
Map<WalletNode, ScriptHashTx[]> nodeHashHistory = new LinkedHashMap<>(nodes.size());
|
||||||
Map<String, String> pathScriptHashes = new LinkedHashMap<>(nodes.size());
|
Map<String, String> pathScriptHashes = new LinkedHashMap<>(nodes.size());
|
||||||
for(WalletNode node : nodes) {
|
for(WalletNode node : nodes) {
|
||||||
if(node.getIndex() >= startIndex) {
|
if(node.getIndex() >= startIndex) {
|
||||||
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(node));
|
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(node));
|
||||||
|
nodeHashHistory.put(node, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -404,43 +417,75 @@ public class ElectrumServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Even if we have some successes, failure to retrieve all references will result in an incomplete wallet history. Don't proceed if that's the case.
|
//Optimistic optimization for confirming transactions by matching against the script hash status should all mempool transactions confirm at the current block height
|
||||||
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
|
for(Map.Entry<WalletNode, ScriptHashTx[]> entry : nodeHashHistory.entrySet()) {
|
||||||
|
WalletNode node = entry.getKey();
|
||||||
|
String scriptHash = pathScriptHashes.get(node.getDerivationPath());
|
||||||
|
List<String> statuses = subscribedScriptHashes.get(scriptHash);
|
||||||
|
|
||||||
for(String path : result.keySet()) {
|
if(statuses != null && !statuses.isEmpty() && AppServices.getCurrentBlockHeight() != null &&
|
||||||
ScriptHashTx[] txes = result.get(path);
|
node.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
|
||||||
|
.anyMatch(txo -> txo.getHeight() <= 0)) {
|
||||||
|
List<ScriptHashTx> scriptHashTxes = getScriptHashes(scriptHash, node);
|
||||||
|
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
|
||||||
|
if(scriptHashTx.height <= 0) {
|
||||||
|
scriptHashTx.height = AppServices.getCurrentBlockHeight();
|
||||||
|
scriptHashTx.fee = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
|
String status = getScriptHashStatus(scriptHashTxes);
|
||||||
if(optionalNode.isPresent()) {
|
if(Objects.equals(status, statuses.getLast())) {
|
||||||
WalletNode node = optionalNode.get();
|
entry.setValue(scriptHashTxes.toArray(new ScriptHashTx[0]));
|
||||||
|
pathScriptHashes.remove(node.getDerivationPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Some servers can return the same tx as multiple ScriptHashTx entries with different heights. Take the highest height only
|
if(!pathScriptHashes.isEmpty()) {
|
||||||
Set<BlockTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash)
|
//Even if we have some successes, failure to retrieve all references will result in an incomplete wallet history. Don't proceed if that's the case.
|
||||||
.collect(TreeSet::new, (set, ref) -> {
|
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
|
||||||
Optional<BlockTransactionHash> optExisting = set.stream().filter(prev -> prev.getHash().equals(ref.getHash())).findFirst();
|
|
||||||
if(optExisting.isPresent()) {
|
for(String path : result.keySet()) {
|
||||||
if(optExisting.get().getHeight() < ref.getHeight()) {
|
ScriptHashTx[] txes = result.get(path);
|
||||||
set.remove(optExisting.get());
|
|
||||||
set.add(ref);
|
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
|
||||||
}
|
if(optionalNode.isPresent()) {
|
||||||
} else {
|
WalletNode node = optionalNode.get();
|
||||||
|
nodeHashHistory.put(node, txes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(WalletNode node : nodeHashHistory.keySet()) {
|
||||||
|
ScriptHashTx[] txes = nodeHashHistory.get(node);
|
||||||
|
|
||||||
|
//Some servers can return the same tx as multiple ScriptHashTx entries with different heights. Take the highest height only
|
||||||
|
Set<BlockTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash)
|
||||||
|
.collect(TreeSet::new, (set, ref) -> {
|
||||||
|
Optional<BlockTransactionHash> optExisting = set.stream().filter(prev -> prev.getHash().equals(ref.getHash())).findFirst();
|
||||||
|
if(optExisting.isPresent()) {
|
||||||
|
if(optExisting.get().getHeight() < ref.getHeight()) {
|
||||||
|
set.remove(optExisting.get());
|
||||||
set.add(ref);
|
set.add(ref);
|
||||||
}
|
}
|
||||||
}, TreeSet::addAll);
|
} else {
|
||||||
Set<BlockTransactionHash> existingReferences = nodeTransactionMap.get(node);
|
set.add(ref);
|
||||||
|
}
|
||||||
|
}, TreeSet::addAll);
|
||||||
|
Set<BlockTransactionHash> existingReferences = nodeTransactionMap.get(node);
|
||||||
|
|
||||||
if(existingReferences == null) {
|
if(existingReferences == null) {
|
||||||
nodeTransactionMap.put(node, references);
|
nodeTransactionMap.put(node, references);
|
||||||
} else {
|
} else {
|
||||||
for(BlockTransactionHash reference : references) {
|
for(BlockTransactionHash reference : references) {
|
||||||
if(!existingReferences.add(reference)) {
|
if(!existingReferences.add(reference)) {
|
||||||
Optional<BlockTransactionHash> optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst();
|
Optional<BlockTransactionHash> optionalReference = existingReferences.stream().filter(tr -> tr.getHash().equals(reference.getHash())).findFirst();
|
||||||
if(optionalReference.isPresent()) {
|
if(optionalReference.isPresent()) {
|
||||||
BlockTransactionHash existingReference = optionalReference.get();
|
BlockTransactionHash existingReference = optionalReference.get();
|
||||||
if(existingReference.getHeight() < reference.getHeight()) {
|
if(existingReference.getHeight() < reference.getHeight()) {
|
||||||
existingReferences.remove(existingReference);
|
existingReferences.remove(existingReference);
|
||||||
existingReferences.add(reference);
|
existingReferences.add(reference);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1539,6 +1584,7 @@ public class ElectrumServer {
|
||||||
private final Sha256Hash txId;
|
private final Sha256Hash txId;
|
||||||
private final Set<WalletNode> nodes;
|
private final Set<WalletNode> nodes;
|
||||||
private final IntegerProperty iterationCount = new SimpleIntegerProperty(0);
|
private final IntegerProperty iterationCount = new SimpleIntegerProperty(0);
|
||||||
|
private boolean cancelled;
|
||||||
|
|
||||||
public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) {
|
public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
|
|
@ -1554,6 +1600,22 @@ public class ElectrumServer {
|
||||||
return iterationCount;
|
return iterationCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
this.cancelled = false;
|
||||||
|
super.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean cancel() {
|
||||||
|
this.cancelled = true;
|
||||||
|
return super.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Task<Set<String>> createTask() {
|
protected Task<Set<String>> createTask() {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ class ScriptHashTx {
|
||||||
public String tx_hash;
|
public String tx_hash;
|
||||||
public long fee;
|
public long fee;
|
||||||
|
|
||||||
|
public ScriptHashTx() {}
|
||||||
|
|
||||||
|
public ScriptHashTx(int height, String tx_hash, long fee) {
|
||||||
|
this.height = height;
|
||||||
|
this.tx_hash = tx_hash;
|
||||||
|
this.fee = fee;
|
||||||
|
}
|
||||||
|
|
||||||
public BlockTransactionHash getBlockchainTransactionHash() {
|
public BlockTransactionHash getBlockchainTransactionHash() {
|
||||||
Sha256Hash hash = Sha256Hash.wrap(tx_hash);
|
Sha256Hash hash = Sha256Hash.wrap(tx_hash);
|
||||||
return new BlockTransaction(hash, height, null, fee, null);
|
return new BlockTransaction(hash, height, null, fee, null);
|
||||||
|
|
|
||||||
|
|
@ -1176,7 +1176,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
|
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(transactionMempoolService.getIterationCount() > 3) {
|
if(transactionMempoolService.getIterationCount() > 3 && !transactionMempoolService.isCancelled()) {
|
||||||
transactionMempoolService.cancel();
|
transactionMempoolService.cancel();
|
||||||
broadcastProgressBar.setProgress(0);
|
broadcastProgressBar.setProgress(0);
|
||||||
log.error("Timeout searching for broadcasted transaction");
|
log.error("Timeout searching for broadcasted transaction");
|
||||||
|
|
@ -1185,11 +1185,13 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
||||||
transactionMempoolService.cancel();
|
if(!transactionMempoolService.isCancelled()) {
|
||||||
broadcastProgressBar.setProgress(0);
|
transactionMempoolService.cancel();
|
||||||
log.error("Timeout searching for broadcasted transaction");
|
broadcastProgressBar.setProgress(0);
|
||||||
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not indicate it had entered the mempool. It is safe to try broadcasting again.");
|
log.error("Timeout searching for broadcasted transaction");
|
||||||
broadcastButton.setDisable(false);
|
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not indicate it had entered the mempool. It is safe to try broadcasting again.");
|
||||||
|
broadcastButton.setDisable(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
transactionMempoolService.start();
|
transactionMempoolService.start();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1332,6 +1334,14 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
super.close();
|
||||||
|
if(transactionMempoolService != null) {
|
||||||
|
transactionMempoolService.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void transactionChanged(TransactionChangedEvent event) {
|
public void transactionChanged(TransactionChangedEvent event) {
|
||||||
if(headersForm.getTransaction().equals(event.getTransaction())) {
|
if(headersForm.getTransaction().equals(event.getTransaction())) {
|
||||||
|
|
@ -1577,24 +1587,18 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
if(transactionMempoolService != null) {
|
if(transactionMempoolService != null) {
|
||||||
transactionMempoolService.cancel();
|
transactionMempoolService.cancel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||||
|
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().equals(event.getWallet()) && headersForm.isTransactionFinalized()) {
|
||||||
Sha256Hash txid = headersForm.getTransaction().getTxId();
|
Sha256Hash txid = headersForm.getTransaction().getTxId();
|
||||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash());
|
BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid);
|
||||||
transactionReferenceService.setOnSucceeded(successEvent -> {
|
if(blockTransaction != null && !blockTransaction.equals(headersForm.getBlockTransaction())) {
|
||||||
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
headersForm.setBlockTransaction(blockTransaction);
|
||||||
BlockTransaction blockTransaction = transactionMap.get(txid);
|
updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight());
|
||||||
if(blockTransaction != null) {
|
}
|
||||||
headersForm.setBlockTransaction(blockTransaction);
|
|
||||||
updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight());
|
|
||||||
}
|
|
||||||
EventManager.get().post(new TransactionReferencesFinishedEvent(headersForm.getTransaction(), blockTransaction));
|
|
||||||
});
|
|
||||||
transactionReferenceService.setOnFailed(failEvent -> {
|
|
||||||
log.error("Could not update block transaction", failEvent.getSource().getException());
|
|
||||||
EventManager.get().post(new TransactionReferencesFailedEvent(headersForm.getTransaction(), failEvent.getSource().getException()));
|
|
||||||
});
|
|
||||||
EventManager.get().post(new TransactionReferencesStartedEvent(headersForm.getTransaction()));
|
|
||||||
transactionReferenceService.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,15 @@ public abstract class TransactionFormController extends BaseController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
|
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
|
||||||
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
|
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
|
||||||
if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) {
|
if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) {
|
||||||
EventManager.get().unregister(this);
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue