introduce nested wallet support to allow child wallets to contribute to the master wallet

This commit is contained in:
Craig Raw 2022-03-02 13:36:38 +02:00
parent ce6b371206
commit 5959b00611
49 changed files with 600 additions and 280 deletions

View file

@ -92,7 +92,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') { implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j' 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:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7') implementation('org.apache.commons:commons-lang3:3.7')
@ -461,7 +461,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor') 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('com.google.common')
requires('net.sourceforge.streamsupport') requires('net.sourceforge.streamsupport')
requires('org.slf4j') requires('org.slf4j')
@ -507,6 +507,7 @@ extraJavaModuleInfo {
exports('com.samourai.whirlpool.protocol.rest') exports('com.samourai.whirlpool.protocol.rest')
exports('com.samourai.whirlpool.client.tx0') exports('com.samourai.whirlpool.client.tx0')
exports('com.samourai.wallet.segwit.bech32') 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.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee') exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState') exports('com.samourai.whirlpool.client.wallet.data.walletState')

2
drongo

@ -1 +1 @@
Subproject commit 956f59880e508127b62d62022e3e2618f659f4d2 Subproject commit 0734757a177627600a63cb3347804ea126b0d417

View file

@ -1046,12 +1046,14 @@ public class AppController implements Initializable {
} }
} }
if(wallet.isBip47()) { for(Wallet childWallet : wallet.getChildWallets()) {
try { if(childWallet.isBip47()) {
Keystore keystore = wallet.getKeystores().get(0); try {
keystore.setBip47ExtendedPrivateKey(wallet.getMasterWallet().getKeystores().get(0).getBip47ExtendedPrivateKey()); Keystore keystore = childWallet.getKeystores().get(0);
} catch(Exception e) { keystore.setBip47ExtendedPrivateKey(wallet.getKeystores().get(0).getBip47ExtendedPrivateKey());
log.error("Cannot prepare BIP47 keystore", e); } catch(Exception e) {
log.error("Cannot prepare BIP47 keystore", e);
}
} }
} }
} }
@ -1183,7 +1185,9 @@ public class AppController implements Initializable {
addWalletTabOrWindow(storage, wallet, false); addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) { for(Wallet childWallet : wallet.getChildWallets()) {
childWallet.encrypt(key); if(!childWallet.isNested()) {
childWallet.encrypt(key);
}
storage.saveWallet(childWallet); storage.saveWallet(childWallet);
checkWalletNetwork(childWallet); checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key); restorePublicKeysFromSeed(storage, childWallet, key);
@ -1488,14 +1492,20 @@ public class AppController implements Initializable {
if(tabData instanceof WalletTabData) { if(tabData instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tabData; WalletTabData walletTabData = (WalletTabData)tabData;
if(walletTabData.getWallet() == wallet.getMasterWallet()) { if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent(); if(wallet.isNested()) {
addWalletSubTab(subTabs, storage, wallet); WalletForm walletForm = new WalletForm(storage, wallet);
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0)); EventManager.get().register(walletForm);
Label masterLabel = (Label)masterTab.getGraphic(); walletTabData.getWalletForm().getNestedWalletForms().add(walletForm);
masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName()); } else {
Platform.runLater(() -> { TabPane subTabs = (TabPane)walletTab.getContent();
setSubTabsVisible(subTabs, true); 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 @Subscribe
public void walletHistoryStarted(WalletHistoryStartedEvent event) { public void walletHistoryStarted(WalletHistoryStartedEvent event) {
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) { 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)); statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) { if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(-1); statusBar.setProgress(-1);
@ -2483,13 +2493,15 @@ public class AppController implements Initializable {
} }
@Subscribe @Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) { public void childWalletsAdded(ChildWalletsAddedEvent event) {
Storage storage = AppServices.get().getOpenWallets().get(event.getWallet()); Storage storage = AppServices.get().getOpenWallets().get(event.getWallet());
if(storage == null) { if(storage == null) {
throw new IllegalStateException("Cannot find storage for master wallet"); throw new IllegalStateException("Cannot find storage for master wallet");
} }
addWalletTab(storage, event.getChildWallet()); for(Wallet childWallet : event.getChildWallets()) {
addWalletTab(storage, childWallet);
}
} }
@Subscribe @Subscribe

View file

@ -689,6 +689,12 @@ public class AppServices {
public static void clearTransactionHistoryCache(Wallet wallet) { public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet); ElectrumServer.clearRetrievedScriptHashes(wallet);
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
AppServices.clearTransactionHistoryCache(childWallet);
}
}
} }
public static boolean isWalletFile(File file) { public static boolean isWalletFile(File file) {

View file

@ -46,7 +46,9 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex()); existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex()); if(!childWallet.isNested()) {
existingIndexes.add(childWallet.getAccountIndex());
}
} }
List<StandardAccount> availableAccounts = new ArrayList<>(); List<StandardAccount> availableAccounts = new ArrayList<>();

View file

@ -48,7 +48,8 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
} }
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) { 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() { public static Glyph getDuplicateGlyph() {

View file

@ -128,7 +128,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}); });
actionBox.getChildren().add(receiveButton); actionBox.getChildren().add(receiveButton);
if(canSignMessage(nodeEntry.getWallet())) { if(canSignMessage(nodeEntry.getNode().getWallet())) {
Button signMessageButton = new Button(""); Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph()); signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> { signMessageButton.setOnAction(event -> {
@ -277,7 +277,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE); WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel(); String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)"; 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))); 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))); 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<Entry, Entry> {
}); });
getItems().add(receiveToAddress); getItems().add(receiveToAddress);
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) { if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message"); MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph()); signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> { signVerifyMessage.setOnAction(AE -> {

View file

@ -89,13 +89,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process * @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) { public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
if(walletNode != null) {
checkWalletSigning(walletNode.getWallet());
}
if(wallet != null) { if(wallet != null) {
if(wallet.getKeystores().size() != 1) { checkWalletSigning(wallet);
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");
}
} }
this.wallet = wallet; this.wallet = wallet;
@ -131,7 +130,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
addressField.getInputs().add(address); addressField.getInputs().add(address);
if(walletNode != null) { if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString()); address.setText(walletNode.getAddress().toString());
} }
Field messageField = new Field(); Field messageField = new Field();
@ -264,6 +263,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}); });
} }
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 { private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText()); return Address.fromString(address.getText());
} }
@ -302,14 +310,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
//Note we can expect a single keystore due to the check in the constructor //Note we can expect a single keystore due to the check in the constructor
if(wallet.getKeystores().get(0).hasPrivateKey()) { Wallet signingWallet = walletNode.getWallet();
if(wallet.isEncrypted()) { if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent()); EventManager.get().post(new RequestOpenWalletsEvent());
} else { } else {
signUnencryptedKeystore(wallet); signUnencryptedKeystore(signingWallet);
} }
} else if(wallet.containsSource(KeystoreSource.HW_USB)) { } else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet); signUsbKeystore(signingWallet);
} }
} }
@ -404,7 +413,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD); WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait(); Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) { 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 -> { decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done")); EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue(); Wallet decryptedWallet = decryptWalletService.getValue();

View file

@ -168,13 +168,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> { toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
if(selectedWallet != null) { if(selectedWallet != null) {
toAddress.setText(selectedWallet.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString()); toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
} }
}); });
keyScriptType.setValue(ScriptType.P2PKH); keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) { 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)); AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));

View file

@ -68,14 +68,16 @@ public class SearchWalletDialog extends Dialog<Entry> {
fieldset.getChildren().addAll(searchField); fieldset.getChildren().addAll(searchField);
form.getChildren().add(fieldset); form.getChildren().add(fieldset);
boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty());
results = new CoinTreeTable(); results = new CoinTreeTable();
results.setShowRoot(false); results.setShowRoot(false);
results.setPrefWidth(walletForms.size() > 1 ? 950 : 850); results.setPrefWidth(showWallet ? 950 : 850);
results.setBitcoinUnit(walletForms.iterator().next().getWallet()); results.setBitcoinUnit(walletForms.iterator().next().getWallet());
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
results.setPlaceholder(new Label("No results")); results.setPlaceholder(new Label("No results"));
if(walletForms.size() > 1) { if(showWallet) {
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet"); TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> { walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName()); return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName());
@ -127,7 +129,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null); setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> { results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) 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) -> { search.textProperty().addListener((observable, oldValue, newValue) -> {
@ -176,6 +179,21 @@ public class SearchWalletDialog extends Dialog<Entry> {
} }
} }
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(); WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
for(Entry entry : walletUtxosEntry.getChildren()) { for(Entry entry : walletUtxosEntry.getChildren()) {
if(entry instanceof HashIndexEntry hashIndexEntry) { if(entry instanceof HashIndexEntry hashIndexEntry) {

View file

@ -209,7 +209,7 @@ public class TransactionDiagram extends GridPane {
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() { private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet()) boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
&& walletTx.getPayments().size() == 1 && 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<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>(); List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) { for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
@ -406,7 +406,9 @@ public class TransactionDiagram extends GridPane {
Long inputValue = null; Long inputValue = null;
if(walletNode != null) { if(walletNode != null) {
inputValue = input.getValue(); 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"); tooltip.getStyleClass().add("input-label");
if(input.getLabel() == null || input.getLabel().isEmpty()) { if(input.getLabel() == null || input.getLabel().isEmpty()) {
@ -648,9 +650,10 @@ public class TransactionDiagram extends GridPane {
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = getToWallet(payment); Wallet toWallet = getToWallet(payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null; 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 ") Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to " + 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.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE); recipientTooltip.setShowDuration(Duration.INDEFINITE);
@ -849,8 +852,28 @@ public class TransactionDiagram extends GridPane {
private Wallet getToWallet(Payment payment) { private Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) { for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isValid() && !openWallet.isBip47() && openWallet.isWalletAddress(payment.getAddress())) { if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
return openWallet; 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;
}
}
}
} }
} }

View file

@ -71,7 +71,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
} }
} }
public void updateHistory(List<WalletNode> updatedNodes) { public void updateHistory() {
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required //Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
sort(); sort();
} }

View file

@ -99,7 +99,7 @@ public class UtxosTreeTable extends CoinTreeTable {
} }
} }
public void updateHistory(List<WalletNode> updatedNodes) { public void updateHistory() {
//Utxo entries should have already been updated, so only a resort required //Utxo entries should have already been updated, so only a resort required
if(!getRoot().getChildren().isEmpty()) { if(!getRoot().getChildren().isEmpty()) {
sort(); sort();

View file

@ -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());
}
}

View file

@ -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<Wallet> 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<Wallet> childWallets) {
super(masterWallet);
this.storage = storage;
this.childWallets = childWallets;
}
public Storage getStorage() {
return storage;
}
public List<Wallet> getChildWallets() {
return childWallets;
}
public String getMasterWalletId() {
return storage.getWalletId(getWallet());
}
}

View file

@ -15,4 +15,20 @@ public class WalletChangedEvent {
public Wallet getWallet() { public Wallet getWallet() {
return wallet; 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);
}
} }

View file

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import java.io.File; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -16,11 +16,13 @@ import java.util.stream.Collectors;
public class WalletHistoryChangedEvent extends WalletChangedEvent { public class WalletHistoryChangedEvent extends WalletChangedEvent {
private final Storage storage; private final Storage storage;
private final List<WalletNode> historyChangedNodes; private final List<WalletNode> historyChangedNodes;
private final List<WalletNode> nestedHistoryChangedNodes;
public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes) { public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes, List<WalletNode> nestedHistoryChangedNodes) {
super(wallet); super(wallet);
this.storage = storage; this.storage = storage;
this.historyChangedNodes = historyChangedNodes; this.historyChangedNodes = historyChangedNodes;
this.nestedHistoryChangedNodes = nestedHistoryChangedNodes;
} }
public String getWalletId() { public String getWalletId() {
@ -31,6 +33,17 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
return historyChangedNodes; return historyChangedNodes;
} }
public List<WalletNode> getNestedHistoryChangedNodes() {
return nestedHistoryChangedNodes;
}
public List<WalletNode> getAllHistoryChangedNodes() {
List<WalletNode> allHistoryChangedNodes = new ArrayList<>(historyChangedNodes.size() + nestedHistoryChangedNodes.size());
allHistoryChangedNodes.addAll(historyChangedNodes);
allHistoryChangedNodes.addAll(nestedHistoryChangedNodes);
return allHistoryChangedNodes;
}
public List<WalletNode> getReceiveNodes() { public List<WalletNode> getReceiveNodes() {
return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList()); return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList());
} }

View file

@ -20,10 +20,17 @@ public class WalletNodeHistoryChangedEvent {
} }
public WalletNode getWalletNode(Wallet wallet) { public WalletNode getWalletNode(Wallet wallet) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) { WalletNode changedNode = getNode(wallet);
WalletNode changedNode = getWalletNode(wallet, keyPurpose); if(changedNode != null) {
if(changedNode != null) { return changedNode;
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; 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) { private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
WalletNode purposeNode = wallet.getNode(keyPurpose); WalletNode purposeNode = wallet.getNode(keyPurpose);
for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) { for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) {

View file

@ -226,7 +226,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
WalletNode purposeNode = wallet.getNode(keyPurpose); WalletNode purposeNode = wallet.getNode(keyPurpose);
purposeNode.fillToIndex(keyPurposes.get(keyPurpose).size() - 1); purposeNode.fillToIndex(keyPurposes.get(keyPurpose).size() - 1);
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
if(address.equals(wallet.getAddress(addressNode))) { if(address.equals(addressNode.getAddress())) {
addressNode.setLabel(ew.labels.get(key)); addressNode.setLabel(ew.labels.get(key));
} }
} }

View file

@ -41,6 +41,7 @@ public class JsonPersistence implements Persistence {
try(Reader reader = new FileReader(storage.getWalletFile())) { try(Reader reader = new FileReader(storage.getWalletFile())) {
wallet = gson.fromJson(reader, Wallet.class); wallet = gson.fromJson(reader, Wallet.class);
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
} }
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null); Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
@ -63,6 +64,7 @@ public class JsonPersistence implements Persistence {
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey); encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8); Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
wallet = gson.fromJson(reader, Wallet.class); wallet = gson.fromJson(reader, Wallet.class);
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
} }
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey); Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
@ -76,6 +78,7 @@ public class JsonPersistence implements Persistence {
Map<WalletAndKey, Storage> childWallets = new TreeMap<>(); Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(File childFile : walletFiles) { for(File childFile : walletFiles) {
Wallet childWallet = loadWallet(childFile, encryptionKey); Wallet childWallet = loadWallet(childFile, encryptionKey);
childWallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(childWallet));
Storage childStorage = new Storage(childFile); Storage childStorage = new Storage(childFile);
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey)); childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
childStorage.setKeyDeriver(getKeyDeriver()); childStorage.setKeyDeriver(getKeyDeriver());

View file

@ -692,7 +692,7 @@ public class DbPersistence implements Persistence {
@Subscribe @Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) { 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())); updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()));
} }
} }

View file

@ -102,6 +102,7 @@ public interface WalletDao {
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId()); List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId());
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList())); wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
wallet.updateTransactions(blockTransactions); wallet.updateTransactions(blockTransactions);

View file

@ -669,7 +669,7 @@ public class ElectrumServer {
Set<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>(); Set<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
//First check all provided txes that pay to this node //First check all provided txes that pay to this node
Script nodeScript = wallet.getOutputScript(node); Script nodeScript = node.getOutputScript();
Set<BlockTransactionHash> history = nodeTransactionMap.get(node); Set<BlockTransactionHash> history = nodeTransactionMap.get(node);
for(BlockTransactionHash reference : history) { for(BlockTransactionHash reference : history) {
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash()); BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
@ -930,7 +930,7 @@ public class ElectrumServer {
} }
public static String getScriptHash(Wallet wallet, WalletNode node) { 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); byte[] reversed = Utils.reverseBytes(hash);
return Utils.bytesToHex(reversed); return Utils.bytesToHex(reversed);
} }
@ -1265,77 +1265,97 @@ public class ElectrumServer {
} }
public static class TransactionHistoryService extends Service<Boolean> { public static class TransactionHistoryService extends Service<Boolean> {
private final Wallet wallet; private final Wallet mainWallet;
private final Set<WalletNode> nodes; private final List<Wallet> filterToWallets;
private final Set<WalletNode> filterToNodes;
private final static Map<Wallet, Object> walletSynchronizeLocks = new HashMap<>(); private final static Map<Wallet, Object> walletSynchronizeLocks = new HashMap<>();
public TransactionHistoryService(Wallet wallet) { public TransactionHistoryService(Wallet wallet) {
this.wallet = wallet; this.mainWallet = wallet;
this.nodes = null; this.filterToWallets = null;
this.filterToNodes = null;
} }
public TransactionHistoryService(Wallet wallet, Set<WalletNode> nodes) { public TransactionHistoryService(Wallet mainWallet, List<Wallet> filterToWallets, Set<WalletNode> filterToNodes) {
this.wallet = wallet; this.mainWallet = mainWallet;
this.nodes = nodes; this.filterToWallets = filterToWallets;
this.filterToNodes = filterToNodes;
} }
@Override @Override
protected Task<Boolean> createTask() { protected Task<Boolean> createTask() {
return new Task<>() { return new Task<>() {
protected Boolean call() throws ServerException { protected Boolean call() throws ServerException {
boolean initial = (walletSynchronizeLocks.putIfAbsent(wallet, new Object()) == null); boolean historyFetched = getTransactionHistory(mainWallet);
synchronized(walletSynchronizeLocks.get(wallet)) { for(Wallet childWallet : new ArrayList<>(mainWallet.getChildWallets())) {
if(initial) { if(childWallet.isNested()) {
addCalculatedScriptHashes(wallet); historyFetched |= getTransactionHistory(childWallet);
} }
if(isConnected()) {
ElectrumServer electrumServer = new ElectrumServer();
Map<String, String> previousScriptHashes = getCalculatedScriptHashes(wallet);
Map<WalletNode, Set<BlockTransactionHash>> 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<WalletNode> updatedNodes = new HashSet<>();
Map<WalletNode, Set<BlockTransactionHashIndex>> 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<WalletNode> nodes = (filterToNodes == null ? null : filterToNodes.stream().filter(node -> node.getWallet().equals(wallet)).collect(Collectors.toSet()));
Map<String, String> previousScriptHashes = getCalculatedScriptHashes(wallet);
Map<WalletNode, Set<BlockTransactionHash>> 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<WalletNode> updatedNodes = new HashSet<>();
Map<WalletNode, Set<BlockTransactionHashIndex>> 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<Set<String>> { public static class TransactionMempoolService extends ScheduledService<Set<String>> {
@ -1696,9 +1716,18 @@ public class ElectrumServer {
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx); Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx);
if(payNym != null) { if(payNym != null) {
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName()); 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 //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); addedWallets.add(addedWallet);
} }
} }

View file

@ -296,7 +296,7 @@ public class PayNymController {
} }
public boolean isLinked(PayNym payNym) { 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; return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
} }
@ -305,7 +305,7 @@ public class PayNymController {
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>(); Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
for(PayNym payNym : following) { for(PayNym payNym : following) {
if(!isLinked(payNym)) { if(!isLinked(payNym)) {
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); PaymentCode externalPaymentCode = payNym.paymentCode();
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode); Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
if(!unlinkedNotification.isEmpty()) { if(!unlinkedNotification.isEmpty()) {
unlinkedNotifications.putAll(unlinkedNotification); unlinkedNotifications.putAll(unlinkedNotification);
@ -345,27 +345,37 @@ public class PayNymController {
} }
private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) { private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) {
List<Wallet> addedWallets = new ArrayList<>();
for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) { for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) {
try { try {
PayNym payNym = unlinkedPayNyms.get(blockTransaction); PayNym payNym = unlinkedPayNyms.get(blockTransaction);
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString()); PaymentCode externalPaymentCode = payNym.paymentCode();
ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(unlinkedNotifications.get(blockTransaction)); WalletNode input0Node = unlinkedNotifications.get(blockTransaction);
TransactionOutPoint input0Outpoint = com.sparrowwallet.drongo.bip47.PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint(); 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()); SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask); byte[] blindedPaymentCode = PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask);
byte[] opReturnData = com.sparrowwallet.drongo.bip47.PaymentCode.getOpReturnData(blockTransaction.getTransaction()); byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction());
if(Arrays.equals(opReturnData, blindedPaymentCode)) { if(Arrays.equals(opReturnData, blindedPaymentCode)) {
addChildWallet(payNym, externalPaymentCode); addedWallets.addAll(addChildWallets(payNym, externalPaymentCode));
followingList.refresh();
} }
} catch(Exception e) { } catch(Exception e) {
log.error("Error adding linked contact from notification transaction", 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<Wallet> addChildWallets(PayNym payNym, PaymentCode externalPaymentCode) {
List<Wallet> addedWallets = new ArrayList<>();
Wallet masterWallet = getMasterWallet(); Wallet masterWallet = getMasterWallet();
Storage storage = AppServices.get().getOpenWallets().get(masterWallet); Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
List<ScriptType> scriptTypes = masterWallet.getScriptType() != ScriptType.P2PKH ? PayNym.getSegwitScriptTypes() : payNym.getScriptTypes(); List<ScriptType> 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()); 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) { public void linkPayNym(PayNym payNym) {
@ -412,7 +424,7 @@ public class PayNymController {
} }
final WalletTransaction walletTx = walletTransaction; final WalletTransaction walletTx = walletTransaction;
final com.sparrowwallet.drongo.bip47.PaymentCode paymentCode = masterWallet.getPaymentCode(); final PaymentCode paymentCode = masterWallet.getPaymentCode();
Wallet wallet = walletTransaction.getWallet(); Wallet wallet = walletTransaction.getWallet();
Storage storage = AppServices.get().getOpenWallets().get(wallet); Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(wallet.isEncrypted()) { 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 { 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(); 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(); TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint();
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey()); SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize()); byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(paymentCode.getPayload(), blindingMask); byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
WalletTransaction finalWalletTx = getWalletTransaction(decryptedWallet, payNym, blindedPaymentCode, walletTransaction.getSelectedUtxos().keySet()); WalletTransaction finalWalletTx = getWalletTransaction(decryptedWallet, payNym, blindedPaymentCode, walletTransaction.getSelectedUtxos().keySet());
PSBT psbt = finalWalletTx.createPSBT(); PSBT psbt = finalWalletTx.createPSBT();
@ -465,11 +478,14 @@ public class PayNymController {
Set<String> scriptHashes = transactionMempoolService.getValue(); Set<String> scriptHashes = transactionMempoolService.getValue();
if(!scriptHashes.isEmpty()) { if(!scriptHashes.isEmpty()) {
transactionMempoolService.cancel(); transactionMempoolService.cancel();
addChildWallet(payNym, externalPaymentCode); List<Wallet> 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); retrievePayNymProgress.setVisible(false);
followingList.refresh(); followingList.refresh();
BlockTransaction blockTransaction = walletTransaction.getWallet().getTransactions().get(transaction.getTxId()); BlockTransaction blockTransaction = walletTransaction.getWallet().getWalletTransaction(transaction.getTxId());
if(blockTransaction != null && blockTransaction.getLabel() == null) { if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel("Link " + payNym.nymName()); blockTransaction.setLabel("Link " + payNym.nymName());
TransactionEntry transactionEntry = new TransactionEntry(walletTransaction.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()); 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<BlockTransactionHashIndex> utxos) throws InsufficientFundsException { private WalletTransaction getWalletTransaction(Wallet wallet, PayNym payNym, byte[] blindedPaymentCode, Collection<BlockTransactionHashIndex> 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); Payment payment = new Payment(externalPaymentCode.getNotificationAddress(), "Link " + payNym.nymName(), MINIMUM_P2PKH_OUTPUT_SATS, false);
List<Payment> payments = List.of(payment); List<Payment> payments = List.of(payment);
List<byte[]> opReturns = List.of(blindedPaymentCode); List<byte[]> opReturns = List.of(blindedPaymentCode);
@ -549,7 +565,7 @@ public class PayNymController {
public void walletHistoryChanged(WalletHistoryChangedEvent event) { public void walletHistoryChanged(WalletHistoryChangedEvent event) {
List<Entry> changedLabelEntries = new ArrayList<>(); List<Entry> changedLabelEntries = new ArrayList<>();
for(Map.Entry<Sha256Hash, PayNym> notificationTx : notificationTransactions.entrySet()) { for(Map.Entry<Sha256Hash, PayNym> notificationTx : notificationTransactions.entrySet()) {
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(notificationTx.getKey()); BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(notificationTx.getKey());
if(blockTransaction != null && blockTransaction.getLabel() == null) { if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel("Link " + notificationTx.getValue().nymName()); blockTransaction.setLabel("Link " + notificationTx.getValue().nymName());
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));

View file

@ -287,7 +287,7 @@ public class CounterpartyController extends SorobanController {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos(); Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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 { try {

View file

@ -433,7 +433,7 @@ public class InitiatorController extends SorobanController {
Payment payment = walletTransaction.getPayments().get(0); Payment payment = walletTransaction.getPayments().get(0);
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos(); Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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); SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);

View file

@ -2,12 +2,16 @@ package com.sparrowwallet.sparrow.soroban;
import com.samourai.soroban.client.SorobanServer; import com.samourai.soroban.client.SorobanServer;
import com.samourai.wallet.api.backend.beans.UnspentOutput; 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.CahootsUtxo;
import com.samourai.wallet.cahoots.SimpleCahootsWallet; import com.samourai.wallet.cahoots.SimpleCahootsWallet;
import com.samourai.wallet.hd.HD_Address; import com.samourai.wallet.hd.HD_Address;
import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.hd.HD_Wallet;
import com.samourai.wallet.send.MyTransactionOutPoint; import com.samourai.wallet.send.MyTransactionOutPoint;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet; 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()); bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
} }
public void addUtxo(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) { public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) {
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(wallet, node, blockTransaction, index); if(node.getWallet().getScriptType() != ScriptType.P2WPKH) {
return;
}
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index);
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams()); 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); addUtxo(account, cahootsUtxo);
} }

View file

@ -406,7 +406,7 @@ public class HeadersController extends TransactionFormController implements Init
} else { } else {
Wallet wallet = getWalletFromTransactionInputs(); Wallet wallet = getWalletFromTransactionInputs();
if(wallet != null) { if(wallet != null) {
feeAmt = calculateFee(wallet.getTransactions()); feeAmt = calculateFee(wallet.getWalletTransactions());
} }
} }
@ -565,7 +565,7 @@ public class HeadersController extends TransactionFormController implements Init
Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions; Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions;
if(walletInputTransactions == null) { if(walletInputTransactions == null) {
Set<Sha256Hash> refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet()); Set<Sha256Hash> 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); walletInputTransactions.keySet().retainAll(refs);
} }
@ -1092,8 +1092,8 @@ public class HeadersController extends TransactionFormController implements Init
public void update() { public void update() {
BlockTransaction blockTransaction = headersForm.getBlockTransaction(); BlockTransaction blockTransaction = headersForm.getBlockTransaction();
Sha256Hash txId = headersForm.getTransaction().getTxId(); Sha256Hash txId = headersForm.getTransaction().getTxId();
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getTransactions().containsKey(txId)) { if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getWalletTransaction(txId) != null) {
blockTransaction = headersForm.getSigningWallet().getTransactions().get(txId); blockTransaction = headersForm.getSigningWallet().getWalletTransaction(txId);
} }
if(blockTransaction != null && AppServices.getCurrentBlockHeight() != null) { if(blockTransaction != null && AppServices.getCurrentBlockHeight() != null) {
@ -1341,7 +1341,7 @@ public class HeadersController extends TransactionFormController implements Init
Sha256Hash txid = headersForm.getTransaction().getTxId(); Sha256Hash txid = headersForm.getTransaction().getTxId();
List<Entry> changedLabelEntries = new ArrayList<>(); List<Entry> changedLabelEntries = new ArrayList<>();
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid); BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid);
if(blockTransaction != null && blockTransaction.getLabel() == null) { if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel(headersForm.getName()); blockTransaction.setLabel(headersForm.getName());
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())); changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));

View file

@ -137,7 +137,7 @@ public class AddressesController extends WalletFormController implements Initial
writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"}); writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"});
for(WalletNode indexNode : purposeNode.getChildren()) { for(WalletNode indexNode : purposeNode.getChildren()) {
writer.write(Integer.toString(indexNode.getIndex())); writer.write(Integer.toString(indexNode.getIndex()));
writer.write(copy.getAddress(indexNode).toString()); writer.write(indexNode.getAddress().toString());
writer.write(getDerivationPath(indexNode)); writer.write(getDerivationPath(indexNode));
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream() Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst(); .filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();

View file

@ -46,6 +46,16 @@ public abstract class Entry {
public abstract Function getWalletFunction(); 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) { public void updateLabel(Entry entry) {
if(this.equals(entry)) { if(this.equals(entry)) {
labelProperty.set(entry.getLabel()); labelProperty.set(entry.getLabel());

View file

@ -20,7 +20,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
private final KeyPurpose keyPurpose; private final KeyPurpose keyPurpose;
public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, 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.hashIndex = hashIndex;
this.type = type; this.type = type;
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
@ -46,7 +46,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
} }
public BlockTransaction getBlockTransaction() { public BlockTransaction getBlockTransaction() {
return getWallet().getTransactions().get(hashIndex.getHash()); return getWallet().getWalletTransaction(hashIndex.getHash());
} }
public String getDescription() { public String getDescription() {
@ -88,7 +88,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
if (this == o) return true; if (this == o) return true;
if (!(o instanceof HashIndexEntry)) return false; if (!(o instanceof HashIndexEntry)) return false;
HashIndexEntry that = (HashIndexEntry) o; HashIndexEntry that = (HashIndexEntry) o;
return getWallet().equals(that.getWallet()) && return super.equals(that) &&
hashIndex.equals(that.hashIndex) && hashIndex.equals(that.hashIndex) &&
type == that.type && type == that.type &&
keyPurpose == that.keyPurpose; keyPurpose == that.keyPurpose;

View file

@ -450,7 +450,7 @@ public class KeystoreController extends WalletFormController implements Initiali
} }
@Subscribe @Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) { public void childWalletsAdded(ChildWalletsAddedEvent event) {
if(event.getMasterWalletId().equals(walletForm.getWalletId())) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH); setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH);
} }

View file

@ -32,15 +32,15 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
} }
public Address getAddress() { public Address getAddress() {
return getWallet().getAddress(node); return node.getAddress();
} }
public Script getOutputScript() { public Script getOutputScript() {
return getWallet().getOutputScript(node); return node.getOutputScript();
} }
public String getOutputDescriptor() { public String getOutputDescriptor() {
return getWallet().getOutputDescriptor(node); return node.getOutputDescriptor();
} }
public void refreshChildren() { public void refreshChildren() {

View file

@ -176,7 +176,7 @@ public class PaymentController extends WalletFormController implements Initializ
} }
} else if(newValue != null) { } else if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE); WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = newValue.getAddress(freshNode); Address freshAddress = freshNode.getAddress();
address.setText(freshAddress.toString()); address.setText(freshAddress.toString());
label.requestFocus(); label.requestFocus();
} }
@ -326,7 +326,7 @@ public class PaymentController extends WalletFormController implements Initializ
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get()); Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get());
if(recipientBip47Wallet != null) { if(recipientBip47Wallet != null) {
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND); WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
ECKey pubKey = recipientBip47Wallet.getPubKey(sendNode); ECKey pubKey = sendNode.getPubKey();
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey); Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) { if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) {
return address; return address;

View file

@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.samourai.whirlpool.client.whirlpool.beans.Pool; import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
@ -606,9 +607,9 @@ public class SendController extends WalletFormController implements Initializabl
OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData(); OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData();
if(optimizationStrategy == OptimizationStrategy.PRIVACY if(optimizationStrategy == OptimizationStrategy.PRIVACY
&& payments.size() == 1 && 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)) { && !(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))); 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) { private void setEffectiveFeeRate(WalletTransaction walletTransaction) {
List<BlockTransaction> unconfirmedUtxoTxs = walletTransaction.getSelectedUtxos().keySet().stream().filter(ref -> ref.getHeight() <= 0) List<BlockTransaction> 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()) { if(!unconfirmedUtxoTxs.isEmpty()) {
long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum(); long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum();
double utxoTxSize = unconfirmedUtxoTxs.stream().mapToDouble(blkTx -> blkTx.getTransaction().getVirtualSize()).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<Payment> payments) { private boolean isMixPossible(List<Payment> payments) {
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet())) return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
&& payments.size() == 1 && 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<Payment> payments) { private void updateOptimizationButtons(List<Payment> payments) {
@ -1141,12 +1142,14 @@ public class SendController extends WalletFormController implements Initializabl
//Ensure all child wallets have been saved //Ensure all child wallets have been saved
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet(); Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
Storage storage = AppServices.get().getOpenWallets().get(childWallet); if(!childWallet.isNested()) {
if(!storage.isPersisted(childWallet)) { Storage storage = AppServices.get().getOpenWallets().get(childWallet);
try { if(!storage.isPersisted(childWallet)) {
storage.saveWallet(childWallet); try {
} catch(Exception e) { storage.saveWallet(childWallet);
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); } catch(Exception e) {
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
} }
} }
} }
@ -1201,8 +1204,8 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) { public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) { if(event.fromThisOrNested(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) {
if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) { if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getAllHistoryChangedNodes())) {
clear(null); clear(null);
} else { } else {
updateTransaction(); updateTransaction();
@ -1232,7 +1235,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) { public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.fromThisOrNested(walletForm.getWallet())) {
updateTransaction(); updateTransaction();
} }
} }
@ -1367,7 +1370,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe @Subscribe
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet())) { if(event.fromThisOrNested(getWalletForm().getWallet())) {
UtxoSelector utxoSelector = utxoSelectorProperty.get(); UtxoSelector utxoSelector = utxoSelectorProperty.get();
if(utxoSelector instanceof MaxUtxoSelector) { if(utxoSelector instanceof MaxUtxoSelector) {
updateTransaction(true); updateTransaction(true);
@ -1424,12 +1427,13 @@ public class SendController extends WalletFormController implements Initializabl
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) { public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
List<Payment> payments = walletTransaction.getPayments(); List<Payment> payments = walletTransaction.getPayments();
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()); List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
Map<Address, WalletNode> walletAddresses = getWalletForm().getWallet().getWalletAddresses();
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy(); OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
boolean payNymPresent = isPayNymMixOnlyPayment(payments); boolean payNymPresent = isPayNymMixOnlyPayment(payments);
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX); boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0); 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 mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty()); boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
if(optimizationStrategy == OptimizationStrategy.PRIVACY) { if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
if(payNymPresent) { if(payNymPresent) {

View file

@ -548,7 +548,7 @@ public class SettingsController extends WalletFormController implements Initiali
Wallet childWallet = masterWallet.addChildWallet(entry.getKey()); Wallet childWallet = masterWallet.addChildWallet(entry.getKey());
childWallet.getKeystores().clear(); childWallet.getKeystores().clear();
childWallet.getKeystores().add(entry.getValue()); 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); saveChildWallets(masterWallet);
} }
@ -556,7 +556,7 @@ public class SettingsController extends WalletFormController implements Initiali
} else { } else {
for(StandardAccount standardAccount : standardAccounts) { for(StandardAccount standardAccount : standardAccounts) {
Wallet childWallet = masterWallet.addChildWallet(standardAccount); 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 { } finally {
masterWallet.encrypt(key); masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isEncrypted()) { if(!childWallet.isNested() && !childWallet.isEncrypted()) {
childWallet.encrypt(key); childWallet.encrypt(key);
} }
} }
@ -587,7 +587,7 @@ public class SettingsController extends WalletFormController implements Initiali
WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
} else { } else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount); 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); saveChildWallets(masterWallet);
@ -595,13 +595,15 @@ public class SettingsController extends WalletFormController implements Initiali
private void saveChildWallets(Wallet masterWallet) { private void saveChildWallets(Wallet masterWallet) {
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
Storage storage = AppServices.get().getOpenWallets().get(childWallet); if(!childWallet.isNested()) {
if(!storage.isPersisted(childWallet)) { Storage storage = AppServices.get().getOpenWallets().get(childWallet);
try { if(!storage.isPersisted(childWallet)) {
storage.saveWallet(childWallet); try {
} catch(Exception e) { storage.saveWallet(childWallet);
log.error("Error saving wallet", e); } catch(Exception e) {
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); 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 @Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) { public void childWalletsAdded(ChildWalletsAddedEvent event) {
if(event.getMasterWalletId().equals(walletForm.getWalletId())) { if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
setInputFieldsDisabled(true); setInputFieldsDisabled(true);
} }
@ -701,7 +703,7 @@ public class SettingsController extends WalletFormController implements Initiali
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET; requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET;
} }
if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && !walletForm.getWallet().getTransactions().isEmpty()) { if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && walletForm.getWallet().hasTransactions()) {
Optional<ButtonType> 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); Optional<ButtonType> 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)) { if(optResponse.isPresent() && optResponse.get().equals(ButtonType.CANCEL)) {
revert.setDisable(false); revert.setDisable(false);
@ -764,7 +766,9 @@ public class SettingsController extends WalletFormController implements Initiali
walletForm.getStorage().setEncryptionPubKey(null); walletForm.getStorage().setEncryptionPubKey(null);
masterWallet.decrypt(key); masterWallet.decrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
childWallet.decrypt(key); if(!childWallet.isNested()) {
childWallet.decrypt(key);
}
} }
saveWallet(true, false); saveWallet(true, false);
return; return;
@ -776,7 +780,9 @@ public class SettingsController extends WalletFormController implements Initiali
masterWallet.encrypt(key); masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) { for(Wallet childWallet : masterWallet.getChildWallets()) {
childWallet.encrypt(key); if(!childWallet.isNested()) {
childWallet.encrypt(key);
}
} }
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
walletForm.saveAndRefresh(); walletForm.saveAndRefresh();

View file

@ -28,7 +28,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
private final BlockTransaction blockTransaction; private final BlockTransaction blockTransaction;
public TransactionEntry(Wallet wallet, BlockTransaction blockTransaction, Map<BlockTransactionHashIndex, KeyPurpose> inputs, Map<BlockTransactionHashIndex, KeyPurpose> outputs) { public TransactionEntry(Wallet wallet, BlockTransaction blockTransaction, Map<BlockTransactionHashIndex, KeyPurpose> inputs, Map<BlockTransactionHashIndex, KeyPurpose> outputs) {
super(wallet, blockTransaction.getLabel(), createChildEntries(wallet, inputs, outputs)); super(wallet.isNested() ? wallet.getMasterWallet() : wallet, blockTransaction.getLabel(), createChildEntries(wallet, inputs, outputs));
this.blockTransaction = blockTransaction; this.blockTransaction = blockTransaction;
labelProperty().addListener((observable, oldValue, newValue) -> { labelProperty().addListener((observable, oldValue, newValue) -> {
@ -169,7 +169,9 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
TransactionEntry that = (TransactionEntry) o; TransactionEntry that = (TransactionEntry) o;
return getWallet().equals(that.getWallet()) && blockTransaction.equals(that.blockTransaction); //Note we check children count only if both are non-zero because we need to match TransactionEntry objects without children for WalletEntryLabelsChangedEvent
return super.equals(that) && blockTransaction.equals(that.blockTransaction)
&& (getChildren().isEmpty() || that.getChildren().isEmpty() || getChildren().size() == that.getChildren().size());
} }
@Override @Override
@ -244,7 +246,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
@Subscribe @Subscribe
public void blockHeightChanged(WalletBlockHeightChangedEvent event) { public void blockHeightChanged(WalletBlockHeightChangedEvent event) {
if(getWallet().equals(event.getWallet())) { if(event.getWallet().equals(getWallet())) {
setConfirmations(calculateConfirmations()); setConfirmations(calculateConfirmations());
if(!isFullyConfirming()) { if(!isFullyConfirming()) {

View file

@ -182,7 +182,7 @@ public class TransactionsController extends WalletFormController implements Init
//Will automatically update transactionsTable transactions and recalculate balances //Will automatically update transactionsTable transactions and recalculate balances
walletTransactionsEntry.updateTransactions(); walletTransactionsEntry.updateTransactions();
transactionsTable.updateHistory(event.getHistoryChangedNodes()); transactionsTable.updateHistory();
balance.setValue(walletTransactionsEntry.getBalance()); balance.setValue(walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance()); mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.update(walletTransactionsEntry); balanceChart.update(walletTransactionsEntry);
@ -192,7 +192,7 @@ public class TransactionsController extends WalletFormController implements Init
@Subscribe @Subscribe
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) { public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.fromThisOrNested(walletForm.getWallet())) {
for(Entry entry : event.getEntries()) { for(Entry entry : event.getEntries()) {
transactionsTable.updateLabel(entry); transactionsTable.updateLabel(entry);
} }
@ -270,7 +270,7 @@ public class TransactionsController extends WalletFormController implements Init
@Subscribe @Subscribe
public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) { public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) {
walletHistoryChanged(new WalletHistoryChangedEvent(getWalletForm().getWallet(), getWalletForm().getStorage(), Collections.emptyList())); walletHistoryChanged(new WalletHistoryChangedEvent(getWalletForm().getWallet(), getWalletForm().getStorage(), Collections.emptyList(), Collections.emptyList()));
} }
@Subscribe @Subscribe

View file

@ -52,7 +52,7 @@ public class UtxoEntry extends HashIndexEntry {
} }
public Address getAddress() { public Address getAddress() {
return getWallet().getAddress(node); return node.getAddress();
} }
public WalletNode getNode() { public WalletNode getNode() {
@ -60,7 +60,7 @@ public class UtxoEntry extends HashIndexEntry {
} }
public String getOutputDescriptor() { public String getOutputDescriptor() {
return getWallet().getOutputDescriptor(node); return node.getOutputDescriptor();
} }
/** /**

View file

@ -283,7 +283,7 @@ public class UtxosController extends WalletFormController implements Initializab
} finally { } finally {
wallet.encrypt(key); wallet.encrypt(key);
for(Wallet childWallet : wallet.getChildWallets()) { for(Wallet childWallet : wallet.getChildWallets()) {
if(!childWallet.isEncrypted()) { if(!childWallet.isNested() && !childWallet.isEncrypted()) {
childWallet.encrypt(key); childWallet.encrypt(key);
} }
} }
@ -340,13 +340,13 @@ public class UtxosController extends WalletFormController implements Initializab
} }
WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE); WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE);
Payment changePayment = new Payment(badbankWallet.getAddress(badbankNode), "Badbank Change", tx0Preview.getChangeValue(), false); Payment changePayment = new Payment(badbankNode.getAddress(), "Badbank Change", tx0Preview.getChangeValue(), false);
payments.add(changePayment); payments.add(changePayment);
WalletNode premixNode = null; WalletNode premixNode = null;
for(int i = 0; i < tx0Preview.getNbPremix(); i++) { for(int i = 0; i < tx0Preview.getNbPremix(); i++) {
premixNode = premixWallet.getFreshNode(KeyPurpose.RECEIVE, premixNode); premixNode = premixWallet.getFreshNode(KeyPurpose.RECEIVE, premixNode);
Address premixAddress = premixWallet.getAddress(premixNode); Address premixAddress = premixNode.getAddress();
payments.add(new Payment(premixAddress, "Premix #" + i, tx0Preview.getPremixValue(), false)); payments.add(new Payment(premixAddress, "Premix #" + i, tx0Preview.getPremixValue(), false));
} }
@ -509,14 +509,14 @@ public class UtxosController extends WalletFormController implements Initializab
} }
updateFields(walletUtxosEntry); updateFields(walletUtxosEntry);
utxosTable.updateHistory(event.getHistoryChangedNodes()); utxosTable.updateHistory();
utxosChart.update(walletUtxosEntry); utxosChart.update(walletUtxosEntry);
} }
} }
@Subscribe @Subscribe
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) { public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.fromThisOrNested(walletForm.getWallet())) {
for(Entry entry : event.getEntries()) { for(Entry entry : event.getEntries()) {
utxosTable.updateLabel(entry); utxosTable.updateLabel(entry);
} }
@ -565,7 +565,7 @@ public class UtxosController extends WalletFormController implements Initializab
@Subscribe @Subscribe
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet())) { if(event.fromThisOrNested(getWalletForm().getWallet())) {
utxosTable.refresh(); utxosTable.refresh();
updateButtons(Config.get().getBitcoinUnit()); updateButtons(Config.get().getBitcoinUnit());
} }

View file

@ -34,6 +34,8 @@ public class WalletForm {
private final Storage storage; private final Storage storage;
protected Wallet wallet; protected Wallet wallet;
private final List<WalletForm> nestedWalletForms = new ArrayList<>();
private WalletTransactionsEntry walletTransactionsEntry; private WalletTransactionsEntry walletTransactionsEntry;
private WalletUtxosEntry walletUtxosEntry; private WalletUtxosEntry walletUtxosEntry;
private final List<NodeEntry> accountEntries = new ArrayList<>(); private final List<NodeEntry> accountEntries = new ArrayList<>();
@ -85,6 +87,10 @@ public class WalletForm {
throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet"); throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet");
} }
public List<WalletForm> getNestedWalletForms() {
return nestedWalletForms;
}
public void revert() { public void revert() {
throw new UnsupportedOperationException("Only SettingsWalletForm supports revert"); throw new UnsupportedOperationException("Only SettingsWalletForm supports revert");
} }
@ -117,10 +123,14 @@ public class WalletForm {
} }
public void refreshHistory(Integer blockHeight) { public void refreshHistory(Integer blockHeight) {
refreshHistory(blockHeight, null); refreshHistory(blockHeight, null, null);
} }
public void refreshHistory(Integer blockHeight, Set<WalletNode> nodes) { public void refreshHistory(Integer blockHeight, Set<WalletNode> nodes) {
refreshHistory(blockHeight, null, nodes);
}
public void refreshHistory(Integer blockHeight, List<Wallet> filterToWallets, Set<WalletNode> nodes) {
Wallet previousWallet = wallet.copy(); Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppServices.isConnected()) { if(wallet.isValid() && AppServices.isConnected()) {
if(log.isDebugEnabled()) { if(log.isDebugEnabled()) {
@ -128,12 +138,12 @@ public class WalletForm {
} }
Set<WalletNode> walletTransactionNodes = getWalletTransactionNodes(nodes); Set<WalletNode> walletTransactionNodes = getWalletTransactionNodes(nodes);
if(walletTransactionNodes == null || !walletTransactionNodes.isEmpty()) { if(!wallet.isNested() && (walletTransactionNodes == null || !walletTransactionNodes.isEmpty())) {
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, walletTransactionNodes); ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, filterToWallets, walletTransactionNodes);
historyService.setOnSucceeded(workerStateEvent -> { historyService.setOnSucceeded(workerStateEvent -> {
if(historyService.getValue()) { if(historyService.getValue()) {
EventManager.get().post(new WalletHistoryFinishedEvent(wallet)); EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
updateWallet(blockHeight, previousWallet); updateWallets(blockHeight, previousWallet);
} }
}); });
historyService.setOnFailed(workerStateEvent -> { historyService.setOnFailed(workerStateEvent -> {
@ -175,8 +185,8 @@ public class WalletForm {
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage()); 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 -> { paymentCodesService.setOnFailed(failedEvent -> {
log.error("Could not determine payment codes for wallet " + wallet.getFullName(), failedEvent.getSource().getException()); 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) { private void updateWallets(Integer blockHeight, Wallet previousWallet) {
if(blockHeight != null) { List<WalletNode> nestedHistoryChangedNodes = new ArrayList<>();
wallet.setStoredBlockHeight(blockHeight); 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<WalletNode> updateWallet(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List<WalletNode> nestedHistoryChangedNodes) {
if(blockHeight != null) {
currentWallet.setStoredBlockHeight(blockHeight);
}
return notifyIfChanged(blockHeight, currentWallet, previousWallet, nestedHistoryChangedNodes);
}
private List<WalletNode> notifyIfChanged(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List<WalletNode> nestedHistoryChangedNodes) {
List<WalletNode> historyChangedNodes = new ArrayList<>(); List<WalletNode> historyChangedNodes = new ArrayList<>();
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren())); historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), currentWallet.getNode(KeyPurpose.RECEIVE).getChildren()));
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren())); historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), currentWallet.getNode(KeyPurpose.CHANGE).getChildren()));
boolean changed = false; boolean changed = false;
if(!historyChangedNodes.isEmpty()) { if(!historyChangedNodes.isEmpty() || !nestedHistoryChangedNodes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes))); Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(currentWallet, storage, historyChangedNodes, nestedHistoryChangedNodes)));
changed = true; if(!historyChangedNodes.isEmpty()) {
changed = true;
}
} }
if(blockHeight != null && !blockHeight.equals(previousWallet.getStoredBlockHeight())) { 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; changed = true;
} }
if(changed) { if(changed) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet))); Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(currentWallet)));
} }
return historyChangedNodes;
} }
private List<WalletNode> getHistoryChangedNodes(Set<WalletNode> previousNodes, Set<WalletNode> currentNodes) { private List<WalletNode> getHistoryChangedNodes(Set<WalletNode> previousNodes, Set<WalletNode> currentNodes) {
@ -390,7 +418,7 @@ public class WalletForm {
public void newBlock(NewBlockEvent event) { public void newBlock(NewBlockEvent event) {
//Check if wallet is valid to avoid saving wallets in initial setup //Check if wallet is valid to avoid saving wallets in initial setup
if(wallet.isValid()) { if(wallet.isValid()) {
updateWallet(event.getHeight(), wallet.copy()); updateWallet(event.getHeight(), wallet, wallet.copy(), Collections.emptyList());
} }
} }
@ -401,7 +429,7 @@ public class WalletForm {
@Subscribe @Subscribe
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) { public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(wallet.isValid()) { if(wallet.isValid() && !wallet.isNested()) {
if(transactionMempoolService != null) { if(transactionMempoolService != null) {
transactionMempoolService.cancel(); transactionMempoolService.cancel();
} }
@ -443,7 +471,7 @@ public class WalletForm {
@Subscribe @Subscribe
public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) { public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) {
if(event.getWallet() == wallet) { if(event.toThisOrNested(wallet)) {
Map<Entry, Entry> labelChangedEntries = new LinkedHashMap<>(); Map<Entry, Entry> labelChangedEntries = new LinkedHashMap<>();
for(Entry entry : event.getEntries()) { for(Entry entry : event.getEntries()) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) { if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
@ -456,8 +484,7 @@ public class WalletForm {
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)")); receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry); 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())) {
if((childNode.getLabel() == null || childNode.getLabel().isEmpty()) && event.getSource(entry) == null) {
childNode.setLabel(entry.getLabel()); childNode.setLabel(entry.getLabel());
labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry); labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry);
} }
@ -481,7 +508,8 @@ public class WalletForm {
} }
if(entry instanceof HashIndexEntry hashIndexEntry) { if(entry instanceof HashIndexEntry hashIndexEntry) {
BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction(); 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()); blockTransaction.setLabel(entry.getLabel());
labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry); labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry);
} }
@ -589,6 +617,9 @@ public class WalletForm {
AppServices.clearTransactionHistoryCache(wallet); AppServices.clearTransactionHistoryCache(wallet);
} }
EventManager.get().unregister(this); EventManager.get().unregister(this);
for(WalletForm nestedWalletForm : nestedWalletForms) {
EventManager.get().unregister(nestedWalletForm);
}
} }
} }
} }
@ -598,4 +629,14 @@ public class WalletForm {
accountEntries.clear(); accountEntries.clear();
EventManager.get().post(new WalletAddressesStatusEvent(wallet)); EventManager.get().post(new WalletAddressesStatusEvent(wallet));
} }
@Subscribe
public void childWalletsAdded(ChildWalletsAddedEvent event) {
if(event.getWallet() == wallet) {
List<Wallet> nestedWallets = event.getChildWallets().stream().filter(Wallet::isNested).collect(Collectors.toList());
if(!nestedWallets.isEmpty()) {
Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), nestedWallets, null));
}
}
}
} }

View file

@ -67,6 +67,9 @@ public class WalletTransactionsEntry extends Entry {
} }
public void updateTransactions() { public void updateTransactions() {
Map<HashIndex, BlockTransactionHashIndex> walletTxos = getWallet().getWalletTxos().entrySet().stream()
.collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey));
List<Entry> current = getWalletTransactions(getWallet()).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList()); List<Entry> current = getWalletTransactions(getWallet()).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList());
List<Entry> previous = new ArrayList<>(getChildren()); List<Entry> previous = new ArrayList<>(getChildren());
@ -80,8 +83,6 @@ public class WalletTransactionsEntry extends Entry {
calculateBalances(true); calculateBalances(true);
Map<HashIndex, BlockTransactionHashIndex> walletTxos = getWallet().getWalletTxos().entrySet().stream()
.collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey));
List<Entry> entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete(walletTxos)).collect(Collectors.toList()); List<Entry> entriesComplete = entriesAdded.stream().filter(txEntry -> ((TransactionEntry)txEntry).isComplete(walletTxos)).collect(Collectors.toList());
if(!entriesComplete.isEmpty()) { if(!entriesComplete.isEmpty()) {
EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), entriesAdded.stream().map(entry -> (TransactionEntry)entry).collect(Collectors.toList()))); 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)); 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<WalletTransaction> walletTransactions = new ArrayList<>(walletTransactionMap.values()); List<WalletTransaction> walletTransactions = new ArrayList<>(walletTransactionMap.values());
Collections.sort(walletTransactions); Collections.sort(walletTransactions);
return walletTransactions; return walletTransactions;
@ -114,7 +123,7 @@ public class WalletTransactionsEntry extends Entry {
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren()); List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
for(WalletNode addressNode : childNodes) { for(WalletNode addressNode : childNodes) {
for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) { 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 //A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again
if(inputTx != null) { if(inputTx != null) {
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx); WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
@ -125,7 +134,7 @@ public class WalletTransactionsEntry extends Entry {
inputWalletTx.incoming.put(hashIndex, keyPurpose); inputWalletTx.incoming.put(hashIndex, keyPurpose);
if(hashIndex.getSpentBy() != null) { if(hashIndex.getSpentBy() != null) {
BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash()); BlockTransaction outputTx = wallet.getWalletTransaction(hashIndex.getSpentBy().getHash());
if(outputTx != null) { if(outputTx != null) {
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx); WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
if(outputWalletTx == null) { if(outputWalletTx == null) {

View file

@ -10,7 +10,7 @@ import java.util.stream.Collectors;
public class WalletUtxosEntry extends Entry { public class WalletUtxosEntry extends Entry {
public WalletUtxosEntry(Wallet wallet) { 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(); calculateDuplicates();
updateMixProgress(); updateMixProgress();
} }
@ -62,7 +62,7 @@ public class WalletUtxosEntry extends Entry {
} }
public void updateUtxos() { public void updateUtxos() {
List<Entry> current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()); List<Entry> current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
List<Entry> previous = new ArrayList<>(getChildren()); List<Entry> previous = new ArrayList<>(getChildren());
List<Entry> entriesAdded = new ArrayList<>(current); List<Entry> entriesAdded = new ArrayList<>(current);

View file

@ -237,7 +237,7 @@ public class Whirlpool {
} }
public MixProgress getMixProgress(BlockTransactionHashIndex utxo) { public MixProgress getMixProgress(BlockTransactionHashIndex utxo) {
if(whirlpoolWalletService.whirlpoolWallet() == null) { if(whirlpoolWalletService.whirlpoolWallet() == null || utxo.getStatus() == Status.FROZEN) {
return null; return null;
} }
@ -409,7 +409,7 @@ public class Whirlpool {
return StandardAccount.ACCOUNT_0; 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); TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
UnspentOutput out = new UnspentOutput(); UnspentOutput out = new UnspentOutput();
@ -431,6 +431,7 @@ public class Whirlpool {
out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight()); out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight());
} }
Wallet wallet = node.getWallet().isBip47() ? node.getWallet().getMasterWallet() : node.getWallet();
if(wallet.getKeystores().size() != 1) { if(wallet.getKeystores().size() != 1) {
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores"); 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) { private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) {
for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) { 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; return walletNode;
} }
} }
@ -638,12 +639,10 @@ public class Whirlpool {
public static class Tx0PreviewsService extends Service<Tx0Previews> { public static class Tx0PreviewsService extends Service<Tx0Previews> {
private final Whirlpool whirlpool; private final Whirlpool whirlpool;
private final Wallet wallet;
private final List<UtxoEntry> utxoEntries; private final List<UtxoEntry> utxoEntries;
public Tx0PreviewsService(Whirlpool whirlpool, Wallet wallet, List<UtxoEntry> utxoEntries) { public Tx0PreviewsService(Whirlpool whirlpool, List<UtxoEntry> utxoEntries) {
this.whirlpool = whirlpool; this.whirlpool = whirlpool;
this.wallet = wallet;
this.utxoEntries = utxoEntries; this.utxoEntries = utxoEntries;
} }
@ -654,7 +653,7 @@ public class Whirlpool {
updateProgress(-1, 1); updateProgress(-1, 1);
updateMessage("Fetching premix preview..."); updateMessage("Fetching premix preview...");
Collection<UnspentOutput> utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(wallet, utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList()); Collection<UnspentOutput> utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList());
return whirlpool.getTx0Previews(utxos); return whirlpool.getTx0Previews(utxos);
} }
}; };

View file

@ -318,7 +318,7 @@ public class WhirlpoolController {
whirlpool.setScode(mixConfig.getScode()); whirlpool.setScode(mixConfig.getScode());
whirlpool.setTx0FeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue())); 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 -> { tx0PreviewsService.setOnRunning(workerStateEvent -> {
nbOutputsBox.setVisible(true); nbOutputsBox.setVisible(true);
nbOutputsLoading.setText("Calculating..."); nbOutputsLoading.setText("Calculating...");

View file

@ -12,9 +12,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.TorService;
import com.sparrowwallet.sparrow.soroban.Soroban; import com.sparrowwallet.sparrow.soroban.Soroban;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
@ -182,7 +180,7 @@ public class WhirlpoolServices {
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) { if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount); Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
EventManager.get().post(new ChildWalletAddedEvent(storage, decryptedWallet, childWallet)); EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
} }
} }
} }

View file

@ -1,15 +1,22 @@
package com.sparrowwallet.sparrow.whirlpool.dataSource; package com.sparrowwallet.sparrow.whirlpool.dataSource;
import com.google.common.eventbus.Subscribe; 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.MinerFeeTarget;
import com.samourai.wallet.api.backend.beans.UnspentOutput; import com.samourai.wallet.api.backend.beans.UnspentOutput;
import com.samourai.wallet.api.backend.beans.WalletResponse; import com.samourai.wallet.api.backend.beans.WalletResponse;
import com.samourai.wallet.hd.HD_Wallet; 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.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.dataPersister.DataPersister;
import com.samourai.whirlpool.client.wallet.data.dataSource.WalletResponseDataSource; 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.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.ExtendedKey;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
@ -79,8 +86,9 @@ public class SparrowDataSource extends WalletResponseDataSource {
continue; continue;
} }
allTransactions.putAll(wallet.getTransactions()); Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
wallet.getTransactions().keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub)); allTransactions.putAll(walletTransactions);
walletTransactions.keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub));
if(wallet.getStoredBlockHeight() != null) { if(wallet.getStoredBlockHeight() != null) {
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight()); 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; 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; 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.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); addresses.add(address);
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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) { 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; 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<BlockTransactionHashIndex, WalletNode> 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 @Override
public void pushTx(String txHex) throws Exception { public void pushTx(String txHex) throws Exception {
Transaction transaction = new Transaction(Utils.hexToBytes(txHex)); Transaction transaction = new Transaction(Utils.hexToBytes(txHex));

View file

@ -39,7 +39,8 @@ public class SparrowPostmixHandler implements IPostmixHandler {
int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex); int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex);
// address // 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()); String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num());
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path); log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);

View file

@ -52,7 +52,7 @@ public class StorageTest extends IoTest {
Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes())); Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes()));
Assert.assertNull(wallet.getKeystores().get(0).getSeed().getMnemonicCode()); 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 @Test