diff --git a/build.gradle b/build.gradle index 7c7c99af..39d46514 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.30') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.32') implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') @@ -461,7 +461,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { exports('co.nstant.in.cbor') } - module('nightjar-0.2.30.jar', 'com.sparrowwallet.nightjar', '0.2.30') { + module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') { requires('com.google.common') requires('net.sourceforge.streamsupport') requires('org.slf4j') @@ -507,6 +507,7 @@ extraJavaModuleInfo { exports('com.samourai.whirlpool.protocol.rest') exports('com.samourai.whirlpool.client.tx0') exports('com.samourai.wallet.segwit.bech32') + exports('com.samourai.whirlpool.client.wallet.data.chain') exports('com.samourai.whirlpool.client.wallet.data.wallet') exports('com.samourai.whirlpool.client.wallet.data.minerFee') exports('com.samourai.whirlpool.client.wallet.data.walletState') diff --git a/drongo b/drongo index 956f5988..0734757a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 956f59880e508127b62d62022e3e2618f659f4d2 +Subproject commit 0734757a177627600a63cb3347804ea126b0d417 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 07592de1..27da823d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1046,12 +1046,14 @@ public class AppController implements Initializable { } } - if(wallet.isBip47()) { - try { - Keystore keystore = wallet.getKeystores().get(0); - keystore.setBip47ExtendedPrivateKey(wallet.getMasterWallet().getKeystores().get(0).getBip47ExtendedPrivateKey()); - } catch(Exception e) { - log.error("Cannot prepare BIP47 keystore", e); + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isBip47()) { + try { + Keystore keystore = childWallet.getKeystores().get(0); + keystore.setBip47ExtendedPrivateKey(wallet.getKeystores().get(0).getBip47ExtendedPrivateKey()); + } catch(Exception e) { + log.error("Cannot prepare BIP47 keystore", e); + } } } } @@ -1183,7 +1185,9 @@ public class AppController implements Initializable { addWalletTabOrWindow(storage, wallet, false); for(Wallet childWallet : wallet.getChildWallets()) { - childWallet.encrypt(key); + if(!childWallet.isNested()) { + childWallet.encrypt(key); + } storage.saveWallet(childWallet); checkWalletNetwork(childWallet); restorePublicKeysFromSeed(storage, childWallet, key); @@ -1488,14 +1492,20 @@ public class AppController implements Initializable { if(tabData instanceof WalletTabData) { WalletTabData walletTabData = (WalletTabData)tabData; if(walletTabData.getWallet() == wallet.getMasterWallet()) { - TabPane subTabs = (TabPane)walletTab.getContent(); - addWalletSubTab(subTabs, storage, wallet); - Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0)); - Label masterLabel = (Label)masterTab.getGraphic(); - masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName()); - Platform.runLater(() -> { - setSubTabsVisible(subTabs, true); - }); + if(wallet.isNested()) { + WalletForm walletForm = new WalletForm(storage, wallet); + EventManager.get().register(walletForm); + walletTabData.getWalletForm().getNestedWalletForms().add(walletForm); + } else { + TabPane subTabs = (TabPane)walletTab.getContent(); + addWalletSubTab(subTabs, storage, wallet); + Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0)); + Label masterLabel = (Label)masterTab.getGraphic(); + masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName()); + Platform.runLater(() -> { + setSubTabsVisible(subTabs, true); + }); + } } } } @@ -2268,7 +2278,7 @@ public class AppController implements Initializable { @Subscribe public void walletHistoryStarted(WalletHistoryStartedEvent event) { if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) { - if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) { + if(event.getWalletNodes() == null && !event.getWallet().hasTransactions()) { statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120)); if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { statusBar.setProgress(-1); @@ -2483,13 +2493,15 @@ public class AppController implements Initializable { } @Subscribe - public void childWalletAdded(ChildWalletAddedEvent event) { + public void childWalletsAdded(ChildWalletsAddedEvent event) { Storage storage = AppServices.get().getOpenWallets().get(event.getWallet()); if(storage == null) { throw new IllegalStateException("Cannot find storage for master wallet"); } - addWalletTab(storage, event.getChildWallet()); + for(Wallet childWallet : event.getChildWallets()) { + addWalletTab(storage, childWallet); + } } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 1cdd8d1c..5307056e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -689,6 +689,12 @@ public class AppServices { public static void clearTransactionHistoryCache(Wallet wallet) { ElectrumServer.clearRetrievedScriptHashes(wallet); + + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isNested()) { + AppServices.clearTransactionHistoryCache(childWallet); + } + } } public static boolean isWalletFile(File file) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java index e5ca895c..27559324 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java @@ -46,7 +46,9 @@ public class AddAccountDialog extends Dialog> { Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); existingIndexes.add(masterWallet.getAccountIndex()); for(Wallet childWallet : masterWallet.getChildWallets()) { - existingIndexes.add(childWallet.getAccountIndex()); + if(!childWallet.isNested()) { + existingIndexes.add(childWallet.getAccountIndex()); + } } List availableAccounts = new ArrayList<>(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java index 28e007d8..4d0f4559 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java @@ -48,7 +48,8 @@ public class AddressCell extends TreeTableCell { } private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) { - return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : ""); + return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) + + utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : ""); } public static Glyph getDuplicateGlyph() { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index aac55763..fd3a5dae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -128,7 +128,7 @@ public class EntryCell extends TreeTableCell { }); actionBox.getChildren().add(receiveButton); - if(canSignMessage(nodeEntry.getWallet())) { + if(canSignMessage(nodeEntry.getNode().getWallet())) { Button signMessageButton = new Button(""); signMessageButton.setGraphic(getSignMessageGlyph()); signMessageButton.setOnAction(event -> { @@ -277,7 +277,7 @@ public class EntryCell extends TreeTableCell { WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE); String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel(); label += (label.isEmpty() ? "" : " ") + "(CPFP)"; - Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true); + Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false))); @@ -507,7 +507,7 @@ public class EntryCell extends TreeTableCell { }); getItems().add(receiveToAddress); - if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) { + if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) { MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message"); signVerifyMessage.setGraphic(getSignMessageGlyph()); signVerifyMessage.setOnAction(AE -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index 1478185a..54a281d5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -89,13 +89,12 @@ public class MessageSignDialog extends Dialog { * @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process */ public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) { + if(walletNode != null) { + checkWalletSigning(walletNode.getWallet()); + } + if(wallet != null) { - if(wallet.getKeystores().size() != 1) { - throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); - } - if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { - throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore"); - } + checkWalletSigning(wallet); } this.wallet = wallet; @@ -131,7 +130,7 @@ public class MessageSignDialog extends Dialog { addressField.getInputs().add(address); if(walletNode != null) { - address.setText(wallet.getAddress(walletNode).toString()); + address.setText(walletNode.getAddress().toString()); } Field messageField = new Field(); @@ -264,6 +263,15 @@ public class MessageSignDialog extends Dialog { }); } + private void checkWalletSigning(Wallet wallet) { + if(wallet.getKeystores().size() != 1) { + throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); + } + if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { + throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore"); + } + } + private Address getAddress()throws InvalidAddressException { return Address.fromString(address.getText()); } @@ -302,14 +310,15 @@ public class MessageSignDialog extends Dialog { } //Note we can expect a single keystore due to the check in the constructor - if(wallet.getKeystores().get(0).hasPrivateKey()) { - if(wallet.isEncrypted()) { + Wallet signingWallet = walletNode.getWallet(); + if(signingWallet.getKeystores().get(0).hasPrivateKey()) { + if(signingWallet.isEncrypted()) { EventManager.get().post(new RequestOpenWalletsEvent()); } else { - signUnencryptedKeystore(wallet); + signUnencryptedKeystore(signingWallet); } - } else if(wallet.containsSource(KeystoreSource.HW_USB)) { - signUsbKeystore(wallet); + } else if(signingWallet.containsSource(KeystoreSource.HW_USB)) { + signUsbKeystore(signingWallet); } } @@ -404,7 +413,7 @@ public class MessageSignDialog extends Dialog { WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD); Optional password = dlg.showAndWait(); if(password.isPresent()) { - Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get()); + Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(walletNode.getWallet().copy(), password.get()); decryptWalletService.setOnSucceeded(workerStateEvent -> { EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done")); Wallet decryptedWallet = decryptWalletService.getValue(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java index 07d78274..c810d030 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java @@ -168,13 +168,13 @@ public class PrivateKeySweepDialog extends Dialog { toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> { if(selectedWallet != null) { - toAddress.setText(selectedWallet.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString()); + toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString()); } }); keyScriptType.setValue(ScriptType.P2PKH); if(wallet != null) { - toAddress.setText(wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString()); + toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString()); } AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null)); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java index 71b51cab..daceb1bb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java @@ -68,14 +68,16 @@ public class SearchWalletDialog extends Dialog { fieldset.getChildren().addAll(searchField); form.getChildren().add(fieldset); + boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty()); + results = new CoinTreeTable(); results.setShowRoot(false); - results.setPrefWidth(walletForms.size() > 1 ? 950 : 850); + results.setPrefWidth(showWallet ? 950 : 850); results.setBitcoinUnit(walletForms.iterator().next().getWallet()); results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); results.setPlaceholder(new Label("No results")); - if(walletForms.size() > 1) { + if(showWallet) { TreeTableColumn walletColumn = new TreeTableColumn<>("Wallet"); walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName()); @@ -127,7 +129,8 @@ public class SearchWalletDialog extends Dialog { setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null); results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener) c -> { - showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty()); + showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty() + || walletForms.stream().map(WalletForm::getWallet).noneMatch(wallet -> wallet == results.getSelectionModel().getSelectedItem().getValue().getWallet())); }); search.textProperty().addListener((observable, oldValue, newValue) -> { @@ -176,6 +179,21 @@ public class SearchWalletDialog extends Dialog { } } + for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) { + for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) { + NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose); + for(Entry entry : purposeEntry.getChildren()) { + if(entry instanceof NodeEntry nodeEntry) { + if(nodeEntry.getAddress().toString().contains(searchText) || + (nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) || + (nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) { + matchingEntries.add(entry); + } + } + } + } + } + WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry(); for(Entry entry : walletUtxosEntry.getChildren()) { if(entry instanceof HashIndexEntry hashIndexEntry) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index cf927d3d..415078e3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -209,7 +209,7 @@ public class TransactionDiagram extends GridPane { private List> getDisplayedUtxoSets() { boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet()) && walletTx.getPayments().size() == 1 - && (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType()); + && (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType()); List> displayedUtxoSets = new ArrayList<>(); for(Map selectedUtxoSet : walletTx.getSelectedUtxoSets()) { @@ -406,7 +406,9 @@ public class TransactionDiagram extends GridPane { Long inputValue = null; if(walletNode != null) { inputValue = input.getValue(); - tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode)); + Wallet nodeWallet = walletNode.getWallet(); + tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" + + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress()); tooltip.getStyleClass().add("input-label"); if(input.getLabel() == null || input.getLabel().isEmpty()) { @@ -648,9 +650,10 @@ public class TransactionDiagram extends GridPane { recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); Wallet toWallet = getToWallet(payment); WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null; + Wallet toBip47Wallet = getBip47SendWallet(payment); Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + getSatsValue(payment.getAmount()) + " sats to " - + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())); + + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())); recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDuration(Duration.INDEFINITE); @@ -849,8 +852,28 @@ public class TransactionDiagram extends GridPane { private Wallet getToWallet(Payment payment) { for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) { - if(openWallet != walletTx.getWallet() && openWallet.isValid() && !openWallet.isBip47() && openWallet.isWalletAddress(payment.getAddress())) { - return openWallet; + if(openWallet != walletTx.getWallet() && openWallet.isValid()) { + WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress()); + if(addressNode != null) { + return addressNode.getWallet(); + } + } + } + + return null; + } + + private Wallet getBip47SendWallet(Payment payment) { + if(walletTx.getWallet() != null) { + for(Wallet childWallet : walletTx.getWallet().getChildWallets()) { + if(childWallet.isNested()) { + WalletNode sendNode = childWallet.getNode(KeyPurpose.SEND); + for(WalletNode sendAddressNode : sendNode.getChildren()) { + if(sendAddressNode.getAddress().equals(payment.getAddress())) { + return childWallet; + } + } + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java index 3409fa02..6f271f38 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java @@ -71,7 +71,7 @@ public class TransactionsTreeTable extends CoinTreeTable { } } - public void updateHistory(List updatedNodes) { + public void updateHistory() { //Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required sort(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java index 5a6b43ad..08c73a73 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java @@ -99,7 +99,7 @@ public class UtxosTreeTable extends CoinTreeTable { } } - public void updateHistory(List updatedNodes) { + public void updateHistory() { //Utxo entries should have already been updated, so only a resort required if(!getRoot().getChildren().isEmpty()) { sort(); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletAddedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletAddedEvent.java deleted file mode 100644 index 1ac2351c..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletAddedEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sparrowwallet.sparrow.event; - -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.sparrow.io.Storage; - -public class ChildWalletAddedEvent extends WalletChangedEvent { - private final Storage storage; - private final Wallet childWallet; - - public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) { - super(masterWallet); - this.storage = storage; - this.childWallet = childWallet; - } - - public Storage getStorage() { - return storage; - } - - public Wallet getChildWallet() { - return childWallet; - } - - public String getMasterWalletId() { - return storage.getWalletId(getWallet()); - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletsAddedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletsAddedEvent.java new file mode 100644 index 00000000..2a18d37e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ChildWalletsAddedEvent.java @@ -0,0 +1,35 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.Storage; + +import java.util.List; + +public class ChildWalletsAddedEvent extends WalletChangedEvent { + private final Storage storage; + private final List childWallets; + + public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) { + super(masterWallet); + this.storage = storage; + this.childWallets = List.of(childWallet); + } + + public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, List childWallets) { + super(masterWallet); + this.storage = storage; + this.childWallets = childWallets; + } + + public Storage getStorage() { + return storage; + } + + public List getChildWallets() { + return childWallets; + } + + public String getMasterWalletId() { + return storage.getWalletId(getWallet()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java index e60a3878..1c134e57 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletChangedEvent.java @@ -15,4 +15,20 @@ public class WalletChangedEvent { public Wallet getWallet() { return wallet; } + + public boolean fromThisOrNested(Wallet targetWallet) { + if(wallet.equals(targetWallet)) { + return true; + } + + return wallet.isNested() && targetWallet.getChildWallets().contains(wallet); + } + + public boolean toThisOrNested(Wallet targetWallet) { + if(wallet.equals(targetWallet)) { + return true; + } + + return targetWallet.isNested() && wallet.getChildWallets().contains(targetWallet); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java index 26441026..73d375f1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java @@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.io.Storage; -import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -16,11 +16,13 @@ import java.util.stream.Collectors; public class WalletHistoryChangedEvent extends WalletChangedEvent { private final Storage storage; private final List historyChangedNodes; + private final List nestedHistoryChangedNodes; - public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List historyChangedNodes) { + public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List historyChangedNodes, List nestedHistoryChangedNodes) { super(wallet); this.storage = storage; this.historyChangedNodes = historyChangedNodes; + this.nestedHistoryChangedNodes = nestedHistoryChangedNodes; } public String getWalletId() { @@ -31,6 +33,17 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent { return historyChangedNodes; } + public List getNestedHistoryChangedNodes() { + return nestedHistoryChangedNodes; + } + + public List getAllHistoryChangedNodes() { + List allHistoryChangedNodes = new ArrayList<>(historyChangedNodes.size() + nestedHistoryChangedNodes.size()); + allHistoryChangedNodes.addAll(historyChangedNodes); + allHistoryChangedNodes.addAll(nestedHistoryChangedNodes); + return allHistoryChangedNodes; + } + public List getReceiveNodes() { return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java index 63c192c2..366ab651 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java @@ -20,10 +20,17 @@ public class WalletNodeHistoryChangedEvent { } public WalletNode getWalletNode(Wallet wallet) { - for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { - WalletNode changedNode = getWalletNode(wallet, keyPurpose); - if(changedNode != null) { - return changedNode; + WalletNode changedNode = getNode(wallet); + if(changedNode != null) { + return changedNode; + } + + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isNested()) { + changedNode = getNode(childWallet); + if(changedNode != null) { + return changedNode; + } } } @@ -38,6 +45,17 @@ public class WalletNodeHistoryChangedEvent { return null; } + private WalletNode getNode(Wallet wallet) { + for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { + WalletNode changedNode = getWalletNode(wallet, keyPurpose); + if(changedNode != null) { + return changedNode; + } + } + + return null; + } + private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) { WalletNode purposeNode = wallet.getNode(keyPurpose); for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java index bbf6f4d2..5ecc3878 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java @@ -226,7 +226,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport WalletNode purposeNode = wallet.getNode(keyPurpose); purposeNode.fillToIndex(keyPurposes.get(keyPurpose).size() - 1); for(WalletNode addressNode : purposeNode.getChildren()) { - if(address.equals(wallet.getAddress(addressNode))) { + if(address.equals(addressNode.getAddress())) { addressNode.setLabel(ew.labels.get(key)); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java index c253ae41..5dde6bda 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java @@ -41,6 +41,7 @@ public class JsonPersistence implements Persistence { try(Reader reader = new FileReader(storage.getWalletFile())) { wallet = gson.fromJson(reader, Wallet.class); + wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet)); } Map childWallets = loadChildWallets(storage, wallet, null); @@ -63,6 +64,7 @@ public class JsonPersistence implements Persistence { encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey); Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8); wallet = gson.fromJson(reader, Wallet.class); + wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet)); } Map childWallets = loadChildWallets(storage, wallet, encryptionKey); @@ -76,6 +78,7 @@ public class JsonPersistence implements Persistence { Map childWallets = new TreeMap<>(); for(File childFile : walletFiles) { Wallet childWallet = loadWallet(childFile, encryptionKey); + childWallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(childWallet)); Storage childStorage = new Storage(childFile); childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey)); childStorage.setKeyDeriver(getKeyDeriver()); 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 c22a6f67..ec725ea7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -692,7 +692,7 @@ public class DbPersistence implements Persistence { @Subscribe public void walletHistoryChanged(WalletHistoryChangedEvent event) { - if(persistsFor(event.getWallet())) { + if(persistsFor(event.getWallet()) && !event.getHistoryChangedNodes().isEmpty()) { updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes())); } } 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 07a7c665..6753d0b3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java @@ -102,6 +102,7 @@ public interface WalletDao { List walletNodes = createWalletNodeDao().getForWalletId(wallet.getId()); wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList())); + wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet)); Map blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); wallet.updateTransactions(blockTransactions); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 405cacfe..ca7253ad 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -669,7 +669,7 @@ public class ElectrumServer { Set transactionOutputs = new TreeSet<>(); //First check all provided txes that pay to this node - Script nodeScript = wallet.getOutputScript(node); + Script nodeScript = node.getOutputScript(); Set history = nodeTransactionMap.get(node); for(BlockTransactionHash reference : history) { BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); @@ -930,7 +930,7 @@ public class ElectrumServer { } public static String getScriptHash(Wallet wallet, WalletNode node) { - byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram()); + byte[] hash = Sha256Hash.hash(node.getOutputScript().getProgram()); byte[] reversed = Utils.reverseBytes(hash); return Utils.bytesToHex(reversed); } @@ -1265,77 +1265,97 @@ public class ElectrumServer { } public static class TransactionHistoryService extends Service { - private final Wallet wallet; - private final Set nodes; + private final Wallet mainWallet; + private final List filterToWallets; + private final Set filterToNodes; private final static Map walletSynchronizeLocks = new HashMap<>(); public TransactionHistoryService(Wallet wallet) { - this.wallet = wallet; - this.nodes = null; + this.mainWallet = wallet; + this.filterToWallets = null; + this.filterToNodes = null; } - public TransactionHistoryService(Wallet wallet, Set nodes) { - this.wallet = wallet; - this.nodes = nodes; + public TransactionHistoryService(Wallet mainWallet, List filterToWallets, Set filterToNodes) { + this.mainWallet = mainWallet; + this.filterToWallets = filterToWallets; + this.filterToNodes = filterToNodes; } @Override protected Task createTask() { return new Task<>() { protected Boolean call() throws ServerException { - boolean initial = (walletSynchronizeLocks.putIfAbsent(wallet, new Object()) == null); - synchronized(walletSynchronizeLocks.get(wallet)) { - if(initial) { - addCalculatedScriptHashes(wallet); + boolean historyFetched = getTransactionHistory(mainWallet); + for(Wallet childWallet : new ArrayList<>(mainWallet.getChildWallets())) { + if(childWallet.isNested()) { + historyFetched |= getTransactionHistory(childWallet); } - - if(isConnected()) { - ElectrumServer electrumServer = new ElectrumServer(); - Map previousScriptHashes = getCalculatedScriptHashes(wallet); - Map> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes)); - electrumServer.getReferencedTransactions(wallet, nodeTransactionMap); - electrumServer.calculateNodeHistory(wallet, nodeTransactionMap); - - //Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes - Set updatedNodes = new HashSet<>(); - Map> walletNodes = wallet.getWalletNodes(); - for(WalletNode node : (nodes == null ? walletNodes.keySet() : nodes)) { - String scriptHash = getScriptHash(wallet, node); - String subscribedStatus = getSubscribedScriptHashStatus(scriptHash); - if(!Objects.equals(subscribedStatus, retrievedScriptHashes.get(scriptHash))) { - updatedNodes.add(node); - } - retrievedScriptHashes.put(scriptHash, subscribedStatus); - } - - //If wallet was not empty, check if all used updated nodes have changed history - if(nodes == null && previousScriptHashes.values().stream().anyMatch(Objects::nonNull)) { - if(!updatedNodes.isEmpty() && updatedNodes.equals(walletNodes.entrySet().stream().filter(entry -> !entry.getValue().isEmpty()).map(Map.Entry::getKey).collect(Collectors.toSet()))) { - //All used nodes on a non-empty wallet have changed history. Abort and trigger a full refresh. - log.info("All used nodes on a non-empty wallet have changed history. Triggering a full wallet refresh."); - throw new AllHistoryChangedException(); - } - } - - //Clear transaction outputs for nodes that have no history - this is useful when a transaction is replaced in the mempool - if(nodes != null) { - for(WalletNode node : nodes) { - String scriptHash = getScriptHash(wallet, node); - if(retrievedScriptHashes.get(scriptHash) == null && !node.getTransactionOutputs().isEmpty()) { - log.debug("Clearing transaction history for " + node); - node.getTransactionOutputs().clear(); - } - } - } - - return true; - } - - return false; } + + return historyFetched; } }; } + + private boolean getTransactionHistory(Wallet wallet) throws ServerException { + if(filterToWallets != null && !filterToWallets.contains(wallet)) { + return false; + } + + boolean initial = (walletSynchronizeLocks.putIfAbsent(wallet, new Object()) == null); + synchronized(walletSynchronizeLocks.get(wallet)) { + if(initial) { + addCalculatedScriptHashes(wallet); + } + + if(isConnected()) { + ElectrumServer electrumServer = new ElectrumServer(); + Set nodes = (filterToNodes == null ? null : filterToNodes.stream().filter(node -> node.getWallet().equals(wallet)).collect(Collectors.toSet())); + + Map previousScriptHashes = getCalculatedScriptHashes(wallet); + Map> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes)); + electrumServer.getReferencedTransactions(wallet, nodeTransactionMap); + electrumServer.calculateNodeHistory(wallet, nodeTransactionMap); + + //Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes + Set updatedNodes = new HashSet<>(); + Map> walletNodes = wallet.getWalletNodes(); + for(WalletNode node : (nodes == null ? walletNodes.keySet() : nodes)) { + String scriptHash = getScriptHash(wallet, node); + String subscribedStatus = getSubscribedScriptHashStatus(scriptHash); + if(!Objects.equals(subscribedStatus, retrievedScriptHashes.get(scriptHash))) { + updatedNodes.add(node); + } + retrievedScriptHashes.put(scriptHash, subscribedStatus); + } + + //If wallet was not empty, check if all used updated nodes have changed history + if(nodes == null && previousScriptHashes.values().stream().anyMatch(Objects::nonNull)) { + if(!updatedNodes.isEmpty() && updatedNodes.equals(walletNodes.entrySet().stream().filter(entry -> !entry.getValue().isEmpty()).map(Map.Entry::getKey).collect(Collectors.toSet()))) { + //All used nodes on a non-empty wallet have changed history. Abort and trigger a full refresh. + log.info("All used nodes on a non-empty wallet have changed history. Triggering a full wallet refresh."); + throw new AllHistoryChangedException(); + } + } + + //Clear transaction outputs for nodes that have no history - this is useful when a transaction is replaced in the mempool + if(nodes != null) { + for(WalletNode node : nodes) { + String scriptHash = getScriptHash(wallet, node); + if(retrievedScriptHashes.get(scriptHash) == null && !node.getTransactionOutputs().isEmpty()) { + log.debug("Clearing transaction history for " + node); + node.getTransactionOutputs().clear(); + } + } + } + + return true; + } + + return false; + } + } } public static class TransactionMempoolService extends ScheduledService> { @@ -1696,9 +1716,18 @@ public class ElectrumServer { Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx); if(payNym != null) { addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName()); + } else { + addedWallet.setLabel(paymentCode.toAbbreviatedString() + " " + childScriptType.getName()); } //Check this is a valid payment code, will throw IllegalArgumentException if not - addedWallet.getPubKey(new WalletNode(KeyPurpose.RECEIVE, 0)); + try { + WalletNode receiveNode = new WalletNode(addedWallet, KeyPurpose.RECEIVE, 0); + receiveNode.getPubKey(); + } catch(IllegalArgumentException e) { + wallet.getChildWallets().remove(addedWallet); + throw e; + } + addedWallets.add(addedWallet); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 927fe5dd..783b087e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -296,7 +296,7 @@ public class PayNymController { } public boolean isLinked(PayNym payNym) { - com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); + PaymentCode externalPaymentCode = payNym.paymentCode(); return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null; } @@ -305,7 +305,7 @@ public class PayNymController { Map unlinkedNotifications = new HashMap<>(); for(PayNym payNym : following) { if(!isLinked(payNym)) { - com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); + PaymentCode externalPaymentCode = payNym.paymentCode(); Map unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode); if(!unlinkedNotification.isEmpty()) { unlinkedNotifications.putAll(unlinkedNotification); @@ -345,27 +345,37 @@ public class PayNymController { } private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map unlinkedPayNyms, Map unlinkedNotifications) { + List addedWallets = new ArrayList<>(); for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) { try { PayNym payNym = unlinkedPayNyms.get(blockTransaction); - com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); - ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(unlinkedNotifications.get(blockTransaction)); - TransactionOutPoint input0Outpoint = com.sparrowwallet.drongo.bip47.PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint(); + PaymentCode externalPaymentCode = payNym.paymentCode(); + WalletNode input0Node = unlinkedNotifications.get(blockTransaction); + Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0); + ECKey input0Key = keystore.getKey(input0Node); + TransactionOutPoint input0Outpoint = PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint(); SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey()); - byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); - byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask); - byte[] opReturnData = com.sparrowwallet.drongo.bip47.PaymentCode.getOpReturnData(blockTransaction.getTransaction()); + byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); + byte[] blindedPaymentCode = PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask); + byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction()); if(Arrays.equals(opReturnData, blindedPaymentCode)) { - addChildWallet(payNym, externalPaymentCode); - followingList.refresh(); + addedWallets.addAll(addChildWallets(payNym, externalPaymentCode)); } } catch(Exception e) { log.error("Error adding linked contact from notification transaction", e); } } + + if(!addedWallets.isEmpty()) { + Wallet masterWallet = getMasterWallet(); + Storage storage = AppServices.get().getOpenWallets().get(masterWallet); + EventManager.get().post(new ChildWalletsAddedEvent(storage, masterWallet, addedWallets)); + followingList.refresh(); + } } - public void addChildWallet(PayNym payNym, com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode) { + public List addChildWallets(PayNym payNym, PaymentCode externalPaymentCode) { + List addedWallets = new ArrayList<>(); Wallet masterWallet = getMasterWallet(); Storage storage = AppServices.get().getOpenWallets().get(masterWallet); List scriptTypes = masterWallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes(); @@ -380,8 +390,10 @@ public class PayNymController { AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage()); } } - EventManager.get().post(new ChildWalletAddedEvent(storage, masterWallet, addedWallet)); + addedWallets.add(addedWallet); } + + return addedWallets; } public void linkPayNym(PayNym payNym) { @@ -412,7 +424,7 @@ public class PayNymController { } final WalletTransaction walletTx = walletTransaction; - final com.sparrowwallet.drongo.bip47.PaymentCode paymentCode = masterWallet.getPaymentCode(); + final PaymentCode paymentCode = masterWallet.getPaymentCode(); Wallet wallet = walletTransaction.getWallet(); Storage storage = AppServices.get().getOpenWallets().get(wallet); if(wallet.isEncrypted()) { @@ -439,15 +451,16 @@ public class PayNymController { } } - private void broadcastNotificationTransaction(Wallet decryptedWallet, WalletTransaction walletTransaction, com.sparrowwallet.drongo.bip47.PaymentCode paymentCode, PayNym payNym) { + private void broadcastNotificationTransaction(Wallet decryptedWallet, WalletTransaction walletTransaction, PaymentCode paymentCode, PayNym payNym) { try { - com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); + PaymentCode externalPaymentCode = payNym.paymentCode(); WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue(); - ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(input0Node); + Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0); + ECKey input0Key = keystore.getKey(input0Node); TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint(); SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey()); - byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); - byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(paymentCode.getPayload(), blindingMask); + byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); + byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask); WalletTransaction finalWalletTx = getWalletTransaction(decryptedWallet, payNym, blindedPaymentCode, walletTransaction.getSelectedUtxos().keySet()); PSBT psbt = finalWalletTx.createPSBT(); @@ -465,11 +478,14 @@ public class PayNymController { Set scriptHashes = transactionMempoolService.getValue(); if(!scriptHashes.isEmpty()) { transactionMempoolService.cancel(); - addChildWallet(payNym, externalPaymentCode); + List addedWallets = addChildWallets(payNym, externalPaymentCode); + Wallet masterWallet = getMasterWallet(); + Storage storage = AppServices.get().getOpenWallets().get(masterWallet); + EventManager.get().post(new ChildWalletsAddedEvent(storage, masterWallet, addedWallets)); retrievePayNymProgress.setVisible(false); followingList.refresh(); - BlockTransaction blockTransaction = walletTransaction.getWallet().getTransactions().get(transaction.getTxId()); + BlockTransaction blockTransaction = walletTransaction.getWallet().getWalletTransaction(transaction.getTxId()); if(blockTransaction != null && blockTransaction.getLabel() == null) { blockTransaction.setLabel("Link " + payNym.nymName()); TransactionEntry transactionEntry = new TransactionEntry(walletTransaction.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()); @@ -512,7 +528,7 @@ public class PayNymController { } private WalletTransaction getWalletTransaction(Wallet wallet, PayNym payNym, byte[] blindedPaymentCode, Collection utxos) throws InsufficientFundsException { - com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); + PaymentCode externalPaymentCode = payNym.paymentCode(); Payment payment = new Payment(externalPaymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false); List payments = List.of(payment); List opReturns = List.of(blindedPaymentCode); @@ -549,7 +565,7 @@ public class PayNymController { public void walletHistoryChanged(WalletHistoryChangedEvent event) { List changedLabelEntries = new ArrayList<>(); for(Map.Entry notificationTx : notificationTransactions.entrySet()) { - BlockTransaction blockTransaction = event.getWallet().getTransactions().get(notificationTx.getKey()); + BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(notificationTx.getKey()); if(blockTransaction != null && blockTransaction.getLabel() == null) { blockTransaction.setLabel("Link " + notificationTx.getValue().nymName()); changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index bcfe44d1..be90d204 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -287,7 +287,7 @@ public class CounterpartyController extends SorobanController { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Map walletUtxos = wallet.getWalletUtxos(); for(Map.Entry entry : walletUtxos.entrySet()) { - counterpartyCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex()); + counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } try { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index bad40fec..a230f599 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -433,7 +433,7 @@ public class InitiatorController extends SorobanController { Payment payment = walletTransaction.getPayments().get(0); Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos(); for(Map.Entry entry : firstSetUtxos.entrySet()) { - initiatorCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex()); + initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java index 5d8a24d4..d49c0bbe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java @@ -2,12 +2,16 @@ package com.sparrowwallet.sparrow.soroban; import com.samourai.soroban.client.SorobanServer; import com.samourai.wallet.api.backend.beans.UnspentOutput; +import com.samourai.wallet.bip47.rpc.PaymentAddress; +import com.samourai.wallet.bip47.rpc.PaymentCode; +import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava; import com.samourai.wallet.cahoots.CahootsUtxo; import com.samourai.wallet.cahoots.SimpleCahootsWallet; import com.samourai.wallet.hd.HD_Address; import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.send.MyTransactionOutPoint; import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; @@ -32,11 +36,29 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet { bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex()); } - public void addUtxo(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) { - UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(wallet, node, blockTransaction, index); + public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) { + if(node.getWallet().getScriptType() != ScriptType.P2WPKH) { + return; + } + + UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index); MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams()); - HD_Address hdAddress = getBip84Wallet().getAddressAt(account, unspentOutput); - CahootsUtxo cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey()); + + CahootsUtxo cahootsUtxo; + if(node.getWallet().isBip47()) { + try { + String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString(); + HD_Address hdAddress = getBip47Wallet().getAccount(getBip47Account()).addressAt(node.getIndex()); + PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, getParams()); + cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), paymentAddress.getReceiveECKey()); + } catch(Exception e) { + throw new IllegalStateException("Cannot add BIP47 UTXO", e); + } + } else { + HD_Address hdAddress = getBip84Wallet().getAddressAt(account, unspentOutput); + cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey()); + } + addUtxo(account, cahootsUtxo); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 35050e3b..8bb2c991 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -406,7 +406,7 @@ public class HeadersController extends TransactionFormController implements Init } else { Wallet wallet = getWalletFromTransactionInputs(); if(wallet != null) { - feeAmt = calculateFee(wallet.getTransactions()); + feeAmt = calculateFee(wallet.getWalletTransactions()); } } @@ -565,7 +565,7 @@ public class HeadersController extends TransactionFormController implements Init Map walletInputTransactions = inputTransactions; if(walletInputTransactions == null) { Set refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet()); - walletInputTransactions = new HashMap<>(wallet.getTransactions()); + walletInputTransactions = wallet.getWalletTransactions(); walletInputTransactions.keySet().retainAll(refs); } @@ -1092,8 +1092,8 @@ public class HeadersController extends TransactionFormController implements Init public void update() { BlockTransaction blockTransaction = headersForm.getBlockTransaction(); Sha256Hash txId = headersForm.getTransaction().getTxId(); - if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getTransactions().containsKey(txId)) { - blockTransaction = headersForm.getSigningWallet().getTransactions().get(txId); + if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getWalletTransaction(txId) != null) { + blockTransaction = headersForm.getSigningWallet().getWalletTransaction(txId); } if(blockTransaction != null && AppServices.getCurrentBlockHeight() != null) { @@ -1341,7 +1341,7 @@ public class HeadersController extends TransactionFormController implements Init Sha256Hash txid = headersForm.getTransaction().getTxId(); List changedLabelEntries = new ArrayList<>(); - BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid); + BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid); if(blockTransaction != null && blockTransaction.getLabel() == null) { blockTransaction.setLabel(headersForm.getName()); changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java index f8387305..ce274099 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java @@ -137,7 +137,7 @@ public class AddressesController extends WalletFormController implements Initial writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"}); for(WalletNode indexNode : purposeNode.getChildren()) { writer.write(Integer.toString(indexNode.getIndex())); - writer.write(copy.getAddress(indexNode).toString()); + writer.write(indexNode.getAddress().toString()); writer.write(getDerivationPath(indexNode)); Optional optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream() .filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java index ac1943d2..a1511836 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java @@ -46,6 +46,16 @@ public abstract class Entry { public abstract Function getWalletFunction(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Entry)) return false; + Entry entry = (Entry) o; + return wallet.equals(entry.wallet) + || (wallet.isNested() && entry.wallet.getChildWallets().contains(wallet)) + || (entry.wallet.isNested() && wallet.getChildWallets().contains(entry.wallet)); + } + public void updateLabel(Entry entry) { if(this.equals(entry)) { labelProperty.set(entry.getLabel()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java index c6444a5c..4375fc09 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java @@ -20,7 +20,7 @@ public class HashIndexEntry extends Entry implements Comparable private final KeyPurpose keyPurpose; public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, KeyPurpose keyPurpose) { - super(wallet, hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT, keyPurpose)) : Collections.emptyList()); + super(wallet.isNested() ? wallet.getMasterWallet() : wallet, hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT, keyPurpose)) : Collections.emptyList()); this.hashIndex = hashIndex; this.type = type; this.keyPurpose = keyPurpose; @@ -46,7 +46,7 @@ public class HashIndexEntry extends Entry implements Comparable } public BlockTransaction getBlockTransaction() { - return getWallet().getTransactions().get(hashIndex.getHash()); + return getWallet().getWalletTransaction(hashIndex.getHash()); } public String getDescription() { @@ -88,7 +88,7 @@ public class HashIndexEntry extends Entry implements Comparable if (this == o) return true; if (!(o instanceof HashIndexEntry)) return false; HashIndexEntry that = (HashIndexEntry) o; - return getWallet().equals(that.getWallet()) && + return super.equals(that) && hashIndex.equals(that.hashIndex) && type == that.type && keyPurpose == that.keyPurpose; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index ce75833b..ccf76d3e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -450,7 +450,7 @@ public class KeystoreController extends WalletFormController implements Initiali } @Subscribe - public void childWalletAdded(ChildWalletAddedEvent event) { + public void childWalletsAdded(ChildWalletsAddedEvent event) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) { setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java index f117ece8..86dba523 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java @@ -32,15 +32,15 @@ public class NodeEntry extends Entry implements Comparable { } public Address getAddress() { - return getWallet().getAddress(node); + return node.getAddress(); } public Script getOutputScript() { - return getWallet().getOutputScript(node); + return node.getOutputScript(); } public String getOutputDescriptor() { - return getWallet().getOutputDescriptor(node); + return node.getOutputDescriptor(); } public void refreshChildren() { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index f828ada9..66f7022d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -176,7 +176,7 @@ public class PaymentController extends WalletFormController implements Initializ } } else if(newValue != null) { WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE); - Address freshAddress = newValue.getAddress(freshNode); + Address freshAddress = freshNode.getAddress(); address.setText(freshAddress.toString()); label.requestFocus(); } @@ -326,7 +326,7 @@ public class PaymentController extends WalletFormController implements Initializ Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get()); if(recipientBip47Wallet != null) { WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND); - ECKey pubKey = recipientBip47Wallet.getPubKey(sendNode); + ECKey pubKey = sendNode.getPubKey(); Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey); if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) { return address; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index fc08c74c..d42ea0ed 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe; import com.samourai.whirlpool.client.whirlpool.beans.Pool; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; @@ -606,9 +607,9 @@ public class SendController extends WalletFormController implements Initializabl OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData(); if(optimizationStrategy == OptimizationStrategy.PRIVACY && payments.size() == 1 - && (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()) + && (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType()) && !(payments.get(0).getAddress() instanceof PayNymAddress)) { - selectors.add(new StonewallUtxoSelector(noInputsFee)); + selectors.add(new StonewallUtxoSelector(payments.get(0).getAddress().getScriptType(), noInputsFee)); } selectors.addAll(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee))); @@ -810,7 +811,7 @@ public class SendController extends WalletFormController implements Initializabl private void setEffectiveFeeRate(WalletTransaction walletTransaction) { List unconfirmedUtxoTxs = walletTransaction.getSelectedUtxos().keySet().stream().filter(ref -> ref.getHeight() <= 0) - .map(ref -> getWalletForm().getWallet().getTransactions().get(ref.getHash())).filter(Objects::nonNull).distinct().collect(Collectors.toList()); + .map(ref -> getWalletForm().getWallet().getWalletTransaction(ref.getHash())).filter(Objects::nonNull).distinct().collect(Collectors.toList()); if(!unconfirmedUtxoTxs.isEmpty()) { long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum(); double utxoTxSize = unconfirmedUtxoTxs.stream().mapToDouble(blkTx -> blkTx.getTransaction().getVirtualSize()).sum(); @@ -966,7 +967,7 @@ public class SendController extends WalletFormController implements Initializabl private boolean isMixPossible(List payments) { return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet())) && payments.size() == 1 - && (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()); + && (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType()); } private void updateOptimizationButtons(List payments) { @@ -1141,12 +1142,14 @@ public class SendController extends WalletFormController implements Initializabl //Ensure all child wallets have been saved Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet(); for(Wallet childWallet : masterWallet.getChildWallets()) { - Storage storage = AppServices.get().getOpenWallets().get(childWallet); - if(!storage.isPersisted(childWallet)) { - try { - storage.saveWallet(childWallet); - } catch(Exception e) { - AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + if(!childWallet.isNested()) { + Storage storage = AppServices.get().getOpenWallets().get(childWallet); + if(!storage.isPersisted(childWallet)) { + try { + storage.saveWallet(childWallet); + } catch(Exception e) { + AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + } } } } @@ -1201,8 +1204,8 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void walletHistoryChanged(WalletHistoryChangedEvent event) { - if(event.getWallet().equals(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) { - if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) { + if(event.fromThisOrNested(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) { + if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getAllHistoryChangedNodes())) { clear(null); } else { updateTransaction(); @@ -1232,7 +1235,7 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) { - if(event.getWallet().equals(walletForm.getWallet())) { + if(event.fromThisOrNested(walletForm.getWallet())) { updateTransaction(); } } @@ -1367,7 +1370,7 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { - if(event.getWallet().equals(getWalletForm().getWallet())) { + if(event.fromThisOrNested(getWalletForm().getWallet())) { UtxoSelector utxoSelector = utxoSelectorProperty.get(); if(utxoSelector instanceof MaxUtxoSelector) { updateTransaction(true); @@ -1424,12 +1427,13 @@ public class SendController extends WalletFormController implements Initializabl public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) { List payments = walletTransaction.getPayments(); List userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); + Map walletAddresses = getWalletForm().getWallet().getWalletAddresses(); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); boolean payNymPresent = isPayNymMixOnlyPayment(payments); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0); - boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()); - boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty()); + boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType()); + boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty()); if(optimizationStrategy == OptimizationStrategy.PRIVACY) { if(payNymPresent) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 56e5657b..c6c20fca 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -548,7 +548,7 @@ public class SettingsController extends WalletFormController implements Initiali Wallet childWallet = masterWallet.addChildWallet(entry.getKey()); childWallet.getKeystores().clear(); childWallet.getKeystores().add(entry.getValue()); - EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); } saveChildWallets(masterWallet); } @@ -556,7 +556,7 @@ public class SettingsController extends WalletFormController implements Initiali } else { for(StandardAccount standardAccount : standardAccounts) { Wallet childWallet = masterWallet.addChildWallet(standardAccount); - EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); } } } @@ -568,7 +568,7 @@ public class SettingsController extends WalletFormController implements Initiali } finally { masterWallet.encrypt(key); for(Wallet childWallet : masterWallet.getChildWallets()) { - if(!childWallet.isEncrypted()) { + if(!childWallet.isNested() && !childWallet.isEncrypted()) { childWallet.encrypt(key); } } @@ -587,7 +587,7 @@ public class SettingsController extends WalletFormController implements Initiali WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); } else { Wallet childWallet = masterWallet.addChildWallet(standardAccount); - EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); } saveChildWallets(masterWallet); @@ -595,13 +595,15 @@ public class SettingsController extends WalletFormController implements Initiali private void saveChildWallets(Wallet masterWallet) { for(Wallet childWallet : masterWallet.getChildWallets()) { - Storage storage = AppServices.get().getOpenWallets().get(childWallet); - if(!storage.isPersisted(childWallet)) { - try { - storage.saveWallet(childWallet); - } catch(Exception e) { - log.error("Error saving wallet", e); - AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + if(!childWallet.isNested()) { + Storage storage = AppServices.get().getOpenWallets().get(childWallet); + if(!storage.isPersisted(childWallet)) { + try { + storage.saveWallet(childWallet); + } catch(Exception e) { + log.error("Error saving wallet", e); + AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + } } } } @@ -679,7 +681,7 @@ public class SettingsController extends WalletFormController implements Initiali } @Subscribe - public void childWalletAdded(ChildWalletAddedEvent event) { + public void childWalletsAdded(ChildWalletsAddedEvent event) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) { setInputFieldsDisabled(true); } @@ -701,7 +703,7 @@ public class SettingsController extends WalletFormController implements Initiali requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET; } - if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && !walletForm.getWallet().getTransactions().isEmpty()) { + if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && walletForm.getWallet().hasTransactions()) { Optional optResponse = AppServices.showWarningDialog("Change Wallet Addresses?", "This wallet has existing transactions which will be replaced as the wallet addresses will change. Ok to proceed?", ButtonType.CANCEL, ButtonType.OK); if(optResponse.isPresent() && optResponse.get().equals(ButtonType.CANCEL)) { revert.setDisable(false); @@ -764,7 +766,9 @@ public class SettingsController extends WalletFormController implements Initiali walletForm.getStorage().setEncryptionPubKey(null); masterWallet.decrypt(key); for(Wallet childWallet : masterWallet.getChildWallets()) { - childWallet.decrypt(key); + if(!childWallet.isNested()) { + childWallet.decrypt(key); + } } saveWallet(true, false); return; @@ -776,7 +780,9 @@ public class SettingsController extends WalletFormController implements Initiali masterWallet.encrypt(key); for(Wallet childWallet : masterWallet.getChildWallets()) { - childWallet.encrypt(key); + if(!childWallet.isNested()) { + childWallet.encrypt(key); + } } walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); walletForm.saveAndRefresh(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java index 68e60a13..ecfb2a34 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java @@ -28,7 +28,7 @@ public class TransactionEntry extends Entry implements Comparable inputs, Map outputs) { - super(wallet, blockTransaction.getLabel(), createChildEntries(wallet, inputs, outputs)); + super(wallet.isNested() ? wallet.getMasterWallet() : wallet, blockTransaction.getLabel(), createChildEntries(wallet, inputs, outputs)); this.blockTransaction = blockTransaction; labelProperty().addListener((observable, oldValue, newValue) -> { @@ -169,7 +169,9 @@ public class TransactionEntry extends Entry implements Comparable nestedWalletForms = new ArrayList<>(); + private WalletTransactionsEntry walletTransactionsEntry; private WalletUtxosEntry walletUtxosEntry; private final List accountEntries = new ArrayList<>(); @@ -85,6 +87,10 @@ public class WalletForm { throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet"); } + public List getNestedWalletForms() { + return nestedWalletForms; + } + public void revert() { throw new UnsupportedOperationException("Only SettingsWalletForm supports revert"); } @@ -117,10 +123,14 @@ public class WalletForm { } public void refreshHistory(Integer blockHeight) { - refreshHistory(blockHeight, null); + refreshHistory(blockHeight, null, null); } public void refreshHistory(Integer blockHeight, Set nodes) { + refreshHistory(blockHeight, null, nodes); + } + + public void refreshHistory(Integer blockHeight, List filterToWallets, Set nodes) { Wallet previousWallet = wallet.copy(); if(wallet.isValid() && AppServices.isConnected()) { if(log.isDebugEnabled()) { @@ -128,12 +138,12 @@ public class WalletForm { } Set walletTransactionNodes = getWalletTransactionNodes(nodes); - if(walletTransactionNodes == null || !walletTransactionNodes.isEmpty()) { - ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, walletTransactionNodes); + if(!wallet.isNested() && (walletTransactionNodes == null || !walletTransactionNodes.isEmpty())) { + ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, filterToWallets, walletTransactionNodes); historyService.setOnSucceeded(workerStateEvent -> { if(historyService.getValue()) { EventManager.get().post(new WalletHistoryFinishedEvent(wallet)); - updateWallet(blockHeight, previousWallet); + updateWallets(blockHeight, previousWallet); } }); historyService.setOnFailed(workerStateEvent -> { @@ -175,8 +185,8 @@ public class WalletForm { AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage()); } } - EventManager.get().post(new ChildWalletAddedEvent(storage, wallet, addedWallet)); } + EventManager.get().post(new ChildWalletsAddedEvent(storage, wallet, addedWallets)); }); paymentCodesService.setOnFailed(failedEvent -> { log.error("Could not determine payment codes for wallet " + wallet.getFullName(), failedEvent.getSource().getException()); @@ -186,33 +196,51 @@ public class WalletForm { } } - private void updateWallet(Integer blockHeight, Wallet previousWallet) { - if(blockHeight != null) { - wallet.setStoredBlockHeight(blockHeight); + private void updateWallets(Integer blockHeight, Wallet previousWallet) { + List nestedHistoryChangedNodes = new ArrayList<>(); + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isNested()) { + Wallet previousChildWallet = previousWallet.getChildWallet(childWallet.getName()); + if(previousChildWallet != null) { + nestedHistoryChangedNodes.addAll(updateWallet(blockHeight, childWallet, previousChildWallet, Collections.emptyList())); + } + } } - notifyIfChanged(blockHeight, previousWallet); + updateWallet(blockHeight, wallet, previousWallet, nestedHistoryChangedNodes); } - private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) { + private List updateWallet(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List nestedHistoryChangedNodes) { + if(blockHeight != null) { + currentWallet.setStoredBlockHeight(blockHeight); + } + + return notifyIfChanged(blockHeight, currentWallet, previousWallet, nestedHistoryChangedNodes); + } + + private List notifyIfChanged(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List nestedHistoryChangedNodes) { List historyChangedNodes = new ArrayList<>(); - historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren())); - historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren())); + historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), currentWallet.getNode(KeyPurpose.RECEIVE).getChildren())); + historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), currentWallet.getNode(KeyPurpose.CHANGE).getChildren())); boolean changed = false; - if(!historyChangedNodes.isEmpty()) { - Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes))); - changed = true; + if(!historyChangedNodes.isEmpty() || !nestedHistoryChangedNodes.isEmpty()) { + Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(currentWallet, storage, historyChangedNodes, nestedHistoryChangedNodes))); + if(!historyChangedNodes.isEmpty()) { + changed = true; + } } if(blockHeight != null && !blockHeight.equals(previousWallet.getStoredBlockHeight())) { - Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(wallet, blockHeight))); + Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(currentWallet, blockHeight))); changed = true; } if(changed) { - Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet))); + Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(currentWallet))); } + + return historyChangedNodes; } private List getHistoryChangedNodes(Set previousNodes, Set currentNodes) { @@ -390,7 +418,7 @@ public class WalletForm { public void newBlock(NewBlockEvent event) { //Check if wallet is valid to avoid saving wallets in initial setup if(wallet.isValid()) { - updateWallet(event.getHeight(), wallet.copy()); + updateWallet(event.getHeight(), wallet, wallet.copy(), Collections.emptyList()); } } @@ -401,7 +429,7 @@ public class WalletForm { @Subscribe public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) { - if(wallet.isValid()) { + if(wallet.isValid() && !wallet.isNested()) { if(transactionMempoolService != null) { transactionMempoolService.cancel(); } @@ -443,7 +471,7 @@ public class WalletForm { @Subscribe public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) { - if(event.getWallet() == wallet) { + if(event.toThisOrNested(wallet)) { Map labelChangedEntries = new LinkedHashMap<>(); for(Entry entry : event.getEntries()) { if(entry.getLabel() != null && !entry.getLabel().isEmpty()) { @@ -456,8 +484,7 @@ public class WalletForm { receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)")); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry); } - //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) { + if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) { childNode.setLabel(entry.getLabel()); labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry); } @@ -481,7 +508,8 @@ public class WalletForm { } if(entry instanceof HashIndexEntry hashIndexEntry) { BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction(); - if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) { + //Avoid recursive changes from hashIndexEntries + if((blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) && event.getSource(entry) == null) { blockTransaction.setLabel(entry.getLabel()); labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry); } @@ -589,6 +617,9 @@ public class WalletForm { AppServices.clearTransactionHistoryCache(wallet); } EventManager.get().unregister(this); + for(WalletForm nestedWalletForm : nestedWalletForms) { + EventManager.get().unregister(nestedWalletForm); + } } } } @@ -598,4 +629,14 @@ public class WalletForm { accountEntries.clear(); EventManager.get().post(new WalletAddressesStatusEvent(wallet)); } + + @Subscribe + public void childWalletsAdded(ChildWalletsAddedEvent event) { + if(event.getWallet() == wallet) { + List nestedWallets = event.getChildWallets().stream().filter(Wallet::isNested).collect(Collectors.toList()); + if(!nestedWallets.isEmpty()) { + Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), nestedWallets, null)); + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index 7f76c1d1..a390027c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -67,6 +67,9 @@ public class WalletTransactionsEntry extends Entry { } public void updateTransactions() { + Map walletTxos = getWallet().getWalletTxos().entrySet().stream() + .collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey)); + List current = getWalletTransactions(getWallet()).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList()); List previous = new ArrayList<>(getChildren()); @@ -80,8 +83,6 @@ public class WalletTransactionsEntry extends Entry { calculateBalances(true); - Map walletTxos = getWallet().getWalletTxos().entrySet().stream() - .collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey)); List entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete(walletTxos)).collect(Collectors.toList()); if(!entriesComplete.isEmpty()) { EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), entriesAdded.stream().map(entry -> (TransactionEntry)entry).collect(Collectors.toList()))); @@ -104,6 +105,14 @@ public class WalletTransactionsEntry extends Entry { getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(keyPurpose)); } + for(Wallet childWallet : wallet.getChildWallets()) { + if(childWallet.isNested()) { + for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { + getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose)); + } + } + } + List walletTransactions = new ArrayList<>(walletTransactionMap.values()); Collections.sort(walletTransactions); return walletTransactions; @@ -114,7 +123,7 @@ public class WalletTransactionsEntry extends Entry { List childNodes = new ArrayList<>(purposeNode.getChildren()); for(WalletNode addressNode : childNodes) { for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) { - BlockTransaction inputTx = wallet.getTransactions().get(hashIndex.getHash()); + BlockTransaction inputTx = wallet.getWalletTransaction(hashIndex.getHash()); //A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again if(inputTx != null) { WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx); @@ -125,7 +134,7 @@ public class WalletTransactionsEntry extends Entry { inputWalletTx.incoming.put(hashIndex, keyPurpose); if(hashIndex.getSpentBy() != null) { - BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash()); + BlockTransaction outputTx = wallet.getWalletTransaction(hashIndex.getSpentBy().getHash()); if(outputTx != null) { WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx); if(outputWalletTx == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java index 0ccfa5c0..6555a6ea 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java @@ -10,7 +10,7 @@ import java.util.stream.Collectors; public class WalletUtxosEntry extends Entry { public WalletUtxosEntry(Wallet wallet) { - super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList())); + super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList())); calculateDuplicates(); updateMixProgress(); } @@ -62,7 +62,7 @@ public class WalletUtxosEntry extends Entry { } public void updateUtxos() { - List current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()); + List current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()); List previous = new ArrayList<>(getChildren()); List entriesAdded = new ArrayList<>(current); diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java index f31c5c37..a1456526 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java @@ -237,7 +237,7 @@ public class Whirlpool { } public MixProgress getMixProgress(BlockTransactionHashIndex utxo) { - if(whirlpoolWalletService.whirlpoolWallet() == null) { + if(whirlpoolWalletService.whirlpoolWallet() == null || utxo.getStatus() == Status.FROZEN) { return null; } @@ -409,7 +409,7 @@ public class Whirlpool { return StandardAccount.ACCOUNT_0; } - public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) { + public static UnspentOutput getUnspentOutput(WalletNode node, BlockTransaction blockTransaction, int index) { TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index); UnspentOutput out = new UnspentOutput(); @@ -431,6 +431,7 @@ public class Whirlpool { out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight()); } + Wallet wallet = node.getWallet().isBip47() ? node.getWallet().getMasterWallet() : node.getWallet(); if(wallet.getKeystores().size() != 1) { throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores"); } @@ -558,7 +559,7 @@ public class Whirlpool { private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) { for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) { - if(walletUtxo.wallet.getAddress(walletNode).toString().equals(e.getMixProgress().getDestination().getAddress())) { + if(walletNode.getAddress().toString().equals(e.getMixProgress().getDestination().getAddress())) { return walletNode; } } @@ -638,12 +639,10 @@ public class Whirlpool { public static class Tx0PreviewsService extends Service { private final Whirlpool whirlpool; - private final Wallet wallet; private final List utxoEntries; - public Tx0PreviewsService(Whirlpool whirlpool, Wallet wallet, List utxoEntries) { + public Tx0PreviewsService(Whirlpool whirlpool, List utxoEntries) { this.whirlpool = whirlpool; - this.wallet = wallet; this.utxoEntries = utxoEntries; } @@ -654,7 +653,7 @@ public class Whirlpool { updateProgress(-1, 1); updateMessage("Fetching premix preview..."); - Collection utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(wallet, utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList()); + Collection utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList()); return whirlpool.getTx0Previews(utxos); } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java index 75aefecc..c3cf3f2c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java @@ -318,7 +318,7 @@ public class WhirlpoolController { whirlpool.setScode(mixConfig.getScode()); whirlpool.setTx0FeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue())); - Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, wallet, utxoEntries); + Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, utxoEntries); tx0PreviewsService.setOnRunning(workerStateEvent -> { nbOutputsBox.setVisible(true); nbOutputsLoading.setText("Calculating..."); diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 3c1d60be..5182e189 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -12,9 +12,7 @@ import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.event.*; -import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; -import com.sparrowwallet.sparrow.net.TorService; import com.sparrowwallet.sparrow.soroban.Soroban; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -182,7 +180,7 @@ public class WhirlpoolServices { for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) { Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount); - EventManager.get().post(new ChildWalletAddedEvent(storage, decryptedWallet, childWallet)); + EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet)); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java index bb133dce..f98cacd2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java @@ -1,15 +1,22 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource; import com.google.common.eventbus.Subscribe; -import com.samourai.wallet.api.backend.MinerFee; import com.samourai.wallet.api.backend.MinerFeeTarget; import com.samourai.wallet.api.backend.beans.UnspentOutput; import com.samourai.wallet.api.backend.beans.WalletResponse; import com.samourai.wallet.hd.HD_Wallet; +import com.samourai.whirlpool.client.tx0.Tx0ParamService; import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; +import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; +import com.samourai.whirlpool.client.wallet.data.chain.ChainSupplier; import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister; import com.samourai.whirlpool.client.wallet.data.dataSource.WalletResponseDataSource; import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier; +import com.samourai.whirlpool.client.wallet.data.pool.PoolSupplier; +import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier; +import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData; +import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier; +import com.samourai.whirlpool.client.wallet.data.wallet.WalletSupplier; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Network; @@ -79,8 +86,9 @@ public class SparrowDataSource extends WalletResponseDataSource { continue; } - allTransactions.putAll(wallet.getTransactions()); - wallet.getTransactions().keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub)); + Map walletTransactions = wallet.getWalletTransactions(); + allTransactions.putAll(walletTransactions); + walletTransactions.keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub)); if(wallet.getStoredBlockHeight() != null) { storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight()); } @@ -93,13 +101,13 @@ public class SparrowDataSource extends WalletResponseDataSource { address.account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex; int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1; address.change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex; - address.n_tx = wallet.getTransactions().size(); + address.n_tx = walletTransactions.size(); addresses.add(address); for(Map.Entry utxo : wallet.getWalletUtxos().entrySet()) { - BlockTransaction blockTransaction = wallet.getTransactions().get(utxo.getKey().getHash()); + BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash()); if(blockTransaction != null && utxo.getKey().getStatus() != Status.FROZEN) { - unspentOutputs.add(Whirlpool.getUnspentOutput(wallet, utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); + unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); } } } @@ -164,6 +172,50 @@ public class SparrowDataSource extends WalletResponseDataSource { return walletResponse; } + @Override + protected BasicUtxoSupplier computeUtxoSupplier(WhirlpoolWallet whirlpoolWallet, WalletSupplier walletSupplier, UtxoConfigSupplier utxoConfigSupplier, ChainSupplier chainSupplier, PoolSupplier poolSupplier, Tx0ParamService tx0ParamService) throws Exception { + return new BasicUtxoSupplier( + walletSupplier, + utxoConfigSupplier, + chainSupplier, + poolSupplier, + tx0ParamService) { + @Override + public void refresh() throws Exception { + SparrowDataSource.this.refresh(); + } + + @Override + protected void onUtxoChanges(UtxoData utxoData) { + super.onUtxoChanges(utxoData); + whirlpoolWallet.onUtxoChanges(utxoData); + } + + @Override + protected byte[] _getPrivKeyBytes(WhirlpoolUtxo whirlpoolUtxo) { + UnspentOutput utxo = whirlpoolUtxo.getUtxo(); + Wallet wallet = getWallet(utxo.xpub.m); + Map walletUtxos = wallet.getWalletUtxos(); + WalletNode node = walletUtxos.entrySet().stream() + .filter(entry -> entry.getKey().getHash().equals(Sha256Hash.wrap(utxo.tx_hash)) && entry.getKey().getIndex() == utxo.tx_output_n) + .map(Map.Entry::getValue) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Cannot find UTXO " + utxo)); + + if(node.getWallet().isBip47()) { + try { + Keystore keystore = node.getWallet().getKeystores().get(0); + return keystore.getKey(node).getPrivKeyBytes(); + } catch(Exception e) { + log.error("Error getting private key", e); + } + } + + return null; + } + }; + } + @Override public void pushTx(String txHex) throws Exception { Transaction transaction = new Transaction(Utils.hexToBytes(txHex)); diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java index e4b63f8c..4ae8f021 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java @@ -39,7 +39,8 @@ public class SparrowPostmixHandler implements IPostmixHandler { int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex); // address - Address address = wallet.getAddress(new WalletNode(keyPurpose, index)); + WalletNode node = new WalletNode(wallet, keyPurpose, index); + Address address = node.getAddress(); String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num()); log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path); diff --git a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java index abee4d8a..362607c3 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/StorageTest.java @@ -52,7 +52,7 @@ public class StorageTest extends IoTest { Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes())); Assert.assertNull(wallet.getKeystores().get(0).getSeed().getMnemonicCode()); - Assert.assertEquals("bc1q2mkrttcuzryrdyn9vtu3nfnt3jlngwn476ktus", wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString()); + Assert.assertEquals("bc1q2mkrttcuzryrdyn9vtu3nfnt3jlngwn476ktus", wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString()); } @Test