mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
introduce nested wallet support to allow child wallets to contribute to the master wallet
This commit is contained in:
parent
ce6b371206
commit
5959b00611
49 changed files with 600 additions and 280 deletions
|
@ -92,7 +92,7 @@ dependencies {
|
|||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.30')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.32')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
|
@ -461,7 +461,7 @@ extraJavaModuleInfo {
|
|||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.30.jar', 'com.sparrowwallet.nightjar', '0.2.30') {
|
||||
module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
|
@ -507,6 +507,7 @@ extraJavaModuleInfo {
|
|||
exports('com.samourai.whirlpool.protocol.rest')
|
||||
exports('com.samourai.whirlpool.client.tx0')
|
||||
exports('com.samourai.wallet.segwit.bech32')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.chain')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit 956f59880e508127b62d62022e3e2618f659f4d2
|
||||
Subproject commit 0734757a177627600a63cb3347804ea126b0d417
|
|
@ -1046,15 +1046,17 @@ public class AppController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
if(wallet.isBip47()) {
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
try {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(wallet.getMasterWallet().getKeystores().get(0).getBip47ExtendedPrivateKey());
|
||||
Keystore keystore = childWallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(wallet.getKeystores().get(0).getBip47ExtendedPrivateKey());
|
||||
} catch(Exception e) {
|
||||
log.error("Cannot prepare BIP47 keystore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void importWallet(ActionEvent event) {
|
||||
WalletImportDialog dlg = new WalletImportDialog();
|
||||
|
@ -1183,7 +1185,9 @@ public class AppController implements Initializable {
|
|||
addWalletTabOrWindow(storage, wallet, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
||||
|
@ -1488,6 +1492,11 @@ public class AppController implements Initializable {
|
|||
if(tabData instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
|
||||
if(wallet.isNested()) {
|
||||
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||
EventManager.get().register(walletForm);
|
||||
walletTabData.getWalletForm().getNestedWalletForms().add(walletForm);
|
||||
} else {
|
||||
TabPane subTabs = (TabPane)walletTab.getContent();
|
||||
addWalletSubTab(subTabs, storage, wallet);
|
||||
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
|
||||
|
@ -1500,6 +1509,7 @@ public class AppController implements Initializable {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
|
||||
}
|
||||
|
@ -2268,7 +2278,7 @@ public class AppController implements Initializable {
|
|||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) {
|
||||
if(event.getWalletNodes() == null && !event.getWallet().hasTransactions()) {
|
||||
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
|
@ -2483,13 +2493,15 @@ public class AppController implements Initializable {
|
|||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletAdded(ChildWalletAddedEvent event) {
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(event.getWallet());
|
||||
if(storage == null) {
|
||||
throw new IllegalStateException("Cannot find storage for master wallet");
|
||||
}
|
||||
|
||||
addWalletTab(storage, event.getChildWallet());
|
||||
for(Wallet childWallet : event.getChildWallets()) {
|
||||
addWalletTab(storage, childWallet);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
@ -689,6 +689,12 @@ public class AppServices {
|
|||
|
||||
public static void clearTransactionHistoryCache(Wallet wallet) {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
AppServices.clearTransactionHistoryCache(childWallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWalletFile(File file) {
|
||||
|
|
|
@ -46,8 +46,10 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
|||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
}
|
||||
}
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||
|
|
|
@ -48,7 +48,8 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -128,7 +128,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(canSignMessage(nodeEntry.getWallet())) {
|
||||
if(canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
|
@ -277,7 +277,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
|
||||
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
|
@ -507,7 +507,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
|||
});
|
||||
getItems().add(receiveToAddress);
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode().getWallet())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||
if(walletNode != null) {
|
||||
checkWalletSigning(walletNode.getWallet());
|
||||
}
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
|
||||
}
|
||||
checkWalletSigning(wallet);
|
||||
}
|
||||
|
||||
this.wallet = wallet;
|
||||
|
@ -131,7 +130,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
address.setText(wallet.getAddress(walletNode).toString());
|
||||
address.setText(walletNode.getAddress().toString());
|
||||
}
|
||||
|
||||
Field messageField = new Field();
|
||||
|
@ -264,6 +263,15 @@ public class MessageSignDialog extends Dialog<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 {
|
||||
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
|
||||
if(wallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
signUnencryptedKeystore(wallet);
|
||||
signUnencryptedKeystore(signingWallet);
|
||||
}
|
||||
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(wallet);
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(signingWallet);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,7 +413,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(walletNode.getWallet().copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
|
|
@ -168,13 +168,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
|||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
|
|
|
@ -68,14 +68,16 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
|||
fieldset.getChildren().addAll(searchField);
|
||||
form.getChildren().add(fieldset);
|
||||
|
||||
boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty());
|
||||
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(walletForms.size() > 1 ? 950 : 850);
|
||||
results.setPrefWidth(showWallet ? 950 : 850);
|
||||
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
if(walletForms.size() > 1) {
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
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);
|
||||
|
||||
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) -> {
|
||||
|
@ -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();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
|
|
|
@ -209,7 +209,7 @@ public class TransactionDiagram extends GridPane {
|
|||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
|
@ -406,7 +406,9 @@ public class TransactionDiagram extends GridPane {
|
|||
Long inputValue = null;
|
||||
if(walletNode != null) {
|
||||
inputValue = input.getValue();
|
||||
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
Wallet nodeWallet = walletNode.getWallet();
|
||||
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" +
|
||||
input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress());
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
|
@ -648,9 +650,10 @@ public class TransactionDiagram extends GridPane {
|
|||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
|
@ -849,8 +852,28 @@ public class TransactionDiagram extends GridPane {
|
|||
|
||||
private Wallet getToWallet(Payment payment) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid() && !openWallet.isBip47() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||
return openWallet;
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Wallet getBip47SendWallet(Payment payment) {
|
||||
if(walletTx.getWallet() != null) {
|
||||
for(Wallet childWallet : walletTx.getWallet().getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
WalletNode sendNode = childWallet.getNode(KeyPurpose.SEND);
|
||||
for(WalletNode sendAddressNode : sendNode.getChildren()) {
|
||||
if(sendAddressNode.getAddress().equals(payment.getAddress())) {
|
||||
return childWallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
sort();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
if(!getRoot().getChildren().isEmpty()) {
|
||||
sort();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -15,4 +15,20 @@ public class WalletChangedEvent {
|
|||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public boolean fromThisOrNested(Wallet targetWallet) {
|
||||
if(wallet.equals(targetWallet)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return wallet.isNested() && targetWallet.getChildWallets().contains(wallet);
|
||||
}
|
||||
|
||||
public boolean toThisOrNested(Wallet targetWallet) {
|
||||
if(wallet.equals(targetWallet)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return targetWallet.isNested() && wallet.getChildWallets().contains(targetWallet);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
|||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -16,11 +16,13 @@ import java.util.stream.Collectors;
|
|||
public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final List<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);
|
||||
this.storage = storage;
|
||||
this.historyChangedNodes = historyChangedNodes;
|
||||
this.nestedHistoryChangedNodes = nestedHistoryChangedNodes;
|
||||
}
|
||||
|
||||
public String getWalletId() {
|
||||
|
@ -31,6 +33,17 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
|||
return historyChangedNodes;
|
||||
}
|
||||
|
||||
public List<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() {
|
||||
return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList());
|
||||
}
|
||||
|
|
|
@ -20,11 +20,18 @@ public class WalletNodeHistoryChangedEvent {
|
|||
}
|
||||
|
||||
public WalletNode getWalletNode(Wallet wallet) {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
|
||||
WalletNode changedNode = getNode(wallet);
|
||||
if(changedNode != null) {
|
||||
return changedNode;
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
changedNode = getNode(childWallet);
|
||||
if(changedNode != null) {
|
||||
return changedNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||
|
@ -38,6 +45,17 @@ public class WalletNodeHistoryChangedEvent {
|
|||
return null;
|
||||
}
|
||||
|
||||
private WalletNode getNode(Wallet wallet) {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
|
||||
if(changedNode != null) {
|
||||
return changedNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
|
||||
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
||||
for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) {
|
||||
|
|
|
@ -226,7 +226,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
|||
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
||||
purposeNode.fillToIndex(keyPurposes.get(keyPurpose).size() - 1);
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
if(address.equals(wallet.getAddress(addressNode))) {
|
||||
if(address.equals(addressNode.getAddress())) {
|
||||
addressNode.setLabel(ew.labels.get(key));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ public class JsonPersistence implements Persistence {
|
|||
|
||||
try(Reader reader = new FileReader(storage.getWalletFile())) {
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
|
||||
}
|
||||
|
||||
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
|
||||
|
@ -63,6 +64,7 @@ public class JsonPersistence implements Persistence {
|
|||
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
|
||||
}
|
||||
|
||||
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
|
@ -76,6 +78,7 @@ public class JsonPersistence implements Persistence {
|
|||
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
|
||||
for(File childFile : walletFiles) {
|
||||
Wallet childWallet = loadWallet(childFile, encryptionKey);
|
||||
childWallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(childWallet));
|
||||
Storage childStorage = new Storage(childFile);
|
||||
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
||||
childStorage.setKeyDeriver(getKeyDeriver());
|
||||
|
|
|
@ -692,7 +692,7 @@ public class DbPersistence implements Persistence {
|
|||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
if(persistsFor(event.getWallet()) && !event.getHistoryChangedNodes().isEmpty()) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ public interface WalletDao {
|
|||
|
||||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId());
|
||||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
|
||||
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
|
||||
wallet.updateTransactions(blockTransactions);
|
||||
|
|
|
@ -669,7 +669,7 @@ public class ElectrumServer {
|
|||
Set<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
|
||||
|
||||
//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);
|
||||
for(BlockTransactionHash reference : history) {
|
||||
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
|
||||
|
@ -930,7 +930,7 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
public static String getScriptHash(Wallet wallet, WalletNode node) {
|
||||
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
||||
byte[] hash = Sha256Hash.hash(node.getOutputScript().getProgram());
|
||||
byte[] reversed = Utils.reverseBytes(hash);
|
||||
return Utils.bytesToHex(reversed);
|
||||
}
|
||||
|
@ -1265,24 +1265,44 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
public static class TransactionHistoryService extends Service<Boolean> {
|
||||
private final Wallet wallet;
|
||||
private final Set<WalletNode> nodes;
|
||||
private final Wallet mainWallet;
|
||||
private final List<Wallet> filterToWallets;
|
||||
private final Set<WalletNode> filterToNodes;
|
||||
private final static Map<Wallet, Object> walletSynchronizeLocks = new HashMap<>();
|
||||
|
||||
public TransactionHistoryService(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
this.nodes = null;
|
||||
this.mainWallet = wallet;
|
||||
this.filterToWallets = null;
|
||||
this.filterToNodes = null;
|
||||
}
|
||||
|
||||
public TransactionHistoryService(Wallet wallet, Set<WalletNode> nodes) {
|
||||
this.wallet = wallet;
|
||||
this.nodes = nodes;
|
||||
public TransactionHistoryService(Wallet mainWallet, List<Wallet> filterToWallets, Set<WalletNode> filterToNodes) {
|
||||
this.mainWallet = mainWallet;
|
||||
this.filterToWallets = filterToWallets;
|
||||
this.filterToNodes = filterToNodes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws ServerException {
|
||||
boolean historyFetched = getTransactionHistory(mainWallet);
|
||||
for(Wallet childWallet : new ArrayList<>(mainWallet.getChildWallets())) {
|
||||
if(childWallet.isNested()) {
|
||||
historyFetched |= getTransactionHistory(childWallet);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -1291,6 +1311,8 @@ public class ElectrumServer {
|
|||
|
||||
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);
|
||||
|
@ -1334,8 +1356,6 @@ public class ElectrumServer {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransactionMempoolService extends ScheduledService<Set<String>> {
|
||||
|
@ -1696,9 +1716,18 @@ public class ElectrumServer {
|
|||
Wallet addedWallet = wallet.addChildWallet(paymentCode, childScriptType, output, blkTx);
|
||||
if(payNym != null) {
|
||||
addedWallet.setLabel(payNym.nymName() + " " + childScriptType.getName());
|
||||
} else {
|
||||
addedWallet.setLabel(paymentCode.toAbbreviatedString() + " " + childScriptType.getName());
|
||||
}
|
||||
//Check this is a valid payment code, will throw IllegalArgumentException if not
|
||||
addedWallet.getPubKey(new WalletNode(KeyPurpose.RECEIVE, 0));
|
||||
try {
|
||||
WalletNode receiveNode = new WalletNode(addedWallet, KeyPurpose.RECEIVE, 0);
|
||||
receiveNode.getPubKey();
|
||||
} catch(IllegalArgumentException e) {
|
||||
wallet.getChildWallets().remove(addedWallet);
|
||||
throw e;
|
||||
}
|
||||
|
||||
addedWallets.add(addedWallet);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -296,7 +296,7 @@ public class PayNymController {
|
|||
}
|
||||
|
||||
public boolean isLinked(PayNym payNym) {
|
||||
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
|
||||
}
|
||||
|
||||
|
@ -305,7 +305,7 @@ public class PayNymController {
|
|||
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
|
||||
for(PayNym payNym : following) {
|
||||
if(!isLinked(payNym)) {
|
||||
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
|
||||
if(!unlinkedNotification.isEmpty()) {
|
||||
unlinkedNotifications.putAll(unlinkedNotification);
|
||||
|
@ -345,27 +345,37 @@ public class PayNymController {
|
|||
}
|
||||
|
||||
private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) {
|
||||
List<Wallet> addedWallets = new ArrayList<>();
|
||||
for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) {
|
||||
try {
|
||||
PayNym payNym = unlinkedPayNyms.get(blockTransaction);
|
||||
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||
ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(unlinkedNotifications.get(blockTransaction));
|
||||
TransactionOutPoint input0Outpoint = com.sparrowwallet.drongo.bip47.PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint();
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
WalletNode input0Node = unlinkedNotifications.get(blockTransaction);
|
||||
Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0);
|
||||
ECKey input0Key = keystore.getKey(input0Node);
|
||||
TransactionOutPoint input0Outpoint = PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint();
|
||||
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
|
||||
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask);
|
||||
byte[] opReturnData = com.sparrowwallet.drongo.bip47.PaymentCode.getOpReturnData(blockTransaction.getTransaction());
|
||||
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||
byte[] blindedPaymentCode = PaymentCode.blind(getMasterWallet().getPaymentCode().getPayload(), blindingMask);
|
||||
byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction());
|
||||
if(Arrays.equals(opReturnData, blindedPaymentCode)) {
|
||||
addChildWallet(payNym, externalPaymentCode);
|
||||
followingList.refresh();
|
||||
addedWallets.addAll(addChildWallets(payNym, externalPaymentCode));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error adding linked contact from notification transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
if(!addedWallets.isEmpty()) {
|
||||
Wallet masterWallet = getMasterWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, masterWallet, addedWallets));
|
||||
followingList.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void addChildWallet(PayNym payNym, com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode) {
|
||||
public List<Wallet> addChildWallets(PayNym payNym, PaymentCode externalPaymentCode) {
|
||||
List<Wallet> addedWallets = new ArrayList<>();
|
||||
Wallet masterWallet = getMasterWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
|
||||
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());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new ChildWalletAddedEvent(storage, masterWallet, addedWallet));
|
||||
addedWallets.add(addedWallet);
|
||||
}
|
||||
|
||||
return addedWallets;
|
||||
}
|
||||
|
||||
public void linkPayNym(PayNym payNym) {
|
||||
|
@ -412,7 +424,7 @@ public class PayNymController {
|
|||
}
|
||||
|
||||
final WalletTransaction walletTx = walletTransaction;
|
||||
final com.sparrowwallet.drongo.bip47.PaymentCode paymentCode = masterWallet.getPaymentCode();
|
||||
final PaymentCode paymentCode = masterWallet.getPaymentCode();
|
||||
Wallet wallet = walletTransaction.getWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
if(wallet.isEncrypted()) {
|
||||
|
@ -439,15 +451,16 @@ public class PayNymController {
|
|||
}
|
||||
}
|
||||
|
||||
private void broadcastNotificationTransaction(Wallet decryptedWallet, WalletTransaction walletTransaction, com.sparrowwallet.drongo.bip47.PaymentCode paymentCode, PayNym payNym) {
|
||||
private void broadcastNotificationTransaction(Wallet decryptedWallet, WalletTransaction walletTransaction, PaymentCode paymentCode, PayNym payNym) {
|
||||
try {
|
||||
com.sparrowwallet.drongo.bip47.PaymentCode externalPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.fromString(payNym.paymentCode().toString());
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||
ECKey input0Key = decryptedWallet.getKeystores().get(0).getKey(input0Node);
|
||||
Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0);
|
||||
ECKey input0Key = keystore.getKey(input0Node);
|
||||
TransactionOutPoint input0Outpoint = walletTransaction.getTransaction().getInputs().iterator().next().getOutpoint();
|
||||
SecretPoint secretPoint = new SecretPoint(input0Key.getPrivKeyBytes(), externalPaymentCode.getNotificationKey().getPubKey());
|
||||
byte[] blindingMask = com.sparrowwallet.drongo.bip47.PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||
byte[] blindedPaymentCode = com.sparrowwallet.drongo.bip47.PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
||||
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), input0Outpoint.bitcoinSerialize());
|
||||
byte[] blindedPaymentCode = PaymentCode.blind(paymentCode.getPayload(), blindingMask);
|
||||
|
||||
WalletTransaction finalWalletTx = getWalletTransaction(decryptedWallet, payNym, blindedPaymentCode, walletTransaction.getSelectedUtxos().keySet());
|
||||
PSBT psbt = finalWalletTx.createPSBT();
|
||||
|
@ -465,11 +478,14 @@ public class PayNymController {
|
|||
Set<String> scriptHashes = transactionMempoolService.getValue();
|
||||
if(!scriptHashes.isEmpty()) {
|
||||
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);
|
||||
followingList.refresh();
|
||||
|
||||
BlockTransaction blockTransaction = walletTransaction.getWallet().getTransactions().get(transaction.getTxId());
|
||||
BlockTransaction blockTransaction = walletTransaction.getWallet().getWalletTransaction(transaction.getTxId());
|
||||
if(blockTransaction != null && blockTransaction.getLabel() == null) {
|
||||
blockTransaction.setLabel("Link " + payNym.nymName());
|
||||
TransactionEntry transactionEntry = new TransactionEntry(walletTransaction.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap());
|
||||
|
@ -512,7 +528,7 @@ public class PayNymController {
|
|||
}
|
||||
|
||||
private WalletTransaction getWalletTransaction(Wallet wallet, PayNym payNym, byte[] blindedPaymentCode, Collection<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);
|
||||
List<Payment> payments = List.of(payment);
|
||||
List<byte[]> opReturns = List.of(blindedPaymentCode);
|
||||
|
@ -549,7 +565,7 @@ public class PayNymController {
|
|||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||
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) {
|
||||
blockTransaction.setLabel("Link " + notificationTx.getValue().nymName());
|
||||
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
|
||||
|
|
|
@ -287,7 +287,7 @@ public class CounterpartyController extends SorobanController {
|
|||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
|
||||
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 {
|
||||
|
|
|
@ -433,7 +433,7 @@ public class InitiatorController extends SorobanController {
|
|||
Payment payment = walletTransaction.getPayments().get(0);
|
||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
|
||||
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);
|
||||
|
|
|
@ -2,12 +2,16 @@ package com.sparrowwallet.sparrow.soroban;
|
|||
|
||||
import com.samourai.soroban.client.SorobanServer;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentAddress;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava;
|
||||
import com.samourai.wallet.cahoots.CahootsUtxo;
|
||||
import com.samourai.wallet.cahoots.SimpleCahootsWallet;
|
||||
import com.samourai.wallet.hd.HD_Address;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.send.MyTransactionOutPoint;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
@ -32,11 +36,29 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
|||
bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
|
||||
}
|
||||
|
||||
public void addUtxo(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(wallet, node, blockTransaction, index);
|
||||
public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
if(node.getWallet().getScriptType() != ScriptType.P2WPKH) {
|
||||
return;
|
||||
}
|
||||
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index);
|
||||
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams());
|
||||
|
||||
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 cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey());
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey());
|
||||
}
|
||||
|
||||
addUtxo(account, cahootsUtxo);
|
||||
}
|
||||
|
||||
|
|
|
@ -406,7 +406,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
} else {
|
||||
Wallet wallet = getWalletFromTransactionInputs();
|
||||
if(wallet != null) {
|
||||
feeAmt = calculateFee(wallet.getTransactions());
|
||||
feeAmt = calculateFee(wallet.getWalletTransactions());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -565,7 +565,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions;
|
||||
if(walletInputTransactions == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1092,8 +1092,8 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
public void update() {
|
||||
BlockTransaction blockTransaction = headersForm.getBlockTransaction();
|
||||
Sha256Hash txId = headersForm.getTransaction().getTxId();
|
||||
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getTransactions().containsKey(txId)) {
|
||||
blockTransaction = headersForm.getSigningWallet().getTransactions().get(txId);
|
||||
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getWalletTransaction(txId) != null) {
|
||||
blockTransaction = headersForm.getSigningWallet().getWalletTransaction(txId);
|
||||
}
|
||||
|
||||
if(blockTransaction != null && AppServices.getCurrentBlockHeight() != null) {
|
||||
|
@ -1341,7 +1341,7 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
Sha256Hash txid = headersForm.getTransaction().getTxId();
|
||||
|
||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid);
|
||||
BlockTransaction blockTransaction = event.getWallet().getWalletTransaction(txid);
|
||||
if(blockTransaction != null && blockTransaction.getLabel() == null) {
|
||||
blockTransaction.setLabel(headersForm.getName());
|
||||
changedLabelEntries.add(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()));
|
||||
|
|
|
@ -137,7 +137,7 @@ public class AddressesController extends WalletFormController implements Initial
|
|||
writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"});
|
||||
for(WalletNode indexNode : purposeNode.getChildren()) {
|
||||
writer.write(Integer.toString(indexNode.getIndex()));
|
||||
writer.write(copy.getAddress(indexNode).toString());
|
||||
writer.write(indexNode.getAddress().toString());
|
||||
writer.write(getDerivationPath(indexNode));
|
||||
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
|
||||
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();
|
||||
|
|
|
@ -46,6 +46,16 @@ public abstract class Entry {
|
|||
|
||||
public abstract Function getWalletFunction();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Entry)) return false;
|
||||
Entry entry = (Entry) o;
|
||||
return wallet.equals(entry.wallet)
|
||||
|| (wallet.isNested() && entry.wallet.getChildWallets().contains(wallet))
|
||||
|| (entry.wallet.isNested() && wallet.getChildWallets().contains(entry.wallet));
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
if(this.equals(entry)) {
|
||||
labelProperty.set(entry.getLabel());
|
||||
|
|
|
@ -20,7 +20,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
|||
private final KeyPurpose keyPurpose;
|
||||
|
||||
public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type, KeyPurpose keyPurpose) {
|
||||
super(wallet, hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT, keyPurpose)) : Collections.emptyList());
|
||||
super(wallet.isNested() ? wallet.getMasterWallet() : wallet, hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT, keyPurpose)) : Collections.emptyList());
|
||||
this.hashIndex = hashIndex;
|
||||
this.type = type;
|
||||
this.keyPurpose = keyPurpose;
|
||||
|
@ -46,7 +46,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
|||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return getWallet().getTransactions().get(hashIndex.getHash());
|
||||
return getWallet().getWalletTransaction(hashIndex.getHash());
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
|
@ -88,7 +88,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
|||
if (this == o) return true;
|
||||
if (!(o instanceof HashIndexEntry)) return false;
|
||||
HashIndexEntry that = (HashIndexEntry) o;
|
||||
return getWallet().equals(that.getWallet()) &&
|
||||
return super.equals(that) &&
|
||||
hashIndex.equals(that.hashIndex) &&
|
||||
type == that.type &&
|
||||
keyPurpose == that.keyPurpose;
|
||||
|
|
|
@ -450,7 +450,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletAdded(ChildWalletAddedEvent event) {
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
|
||||
setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH);
|
||||
}
|
||||
|
|
|
@ -32,15 +32,15 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
|||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return getWallet().getAddress(node);
|
||||
return node.getAddress();
|
||||
}
|
||||
|
||||
public Script getOutputScript() {
|
||||
return getWallet().getOutputScript(node);
|
||||
return node.getOutputScript();
|
||||
}
|
||||
|
||||
public String getOutputDescriptor() {
|
||||
return getWallet().getOutputDescriptor(node);
|
||||
return node.getOutputDescriptor();
|
||||
}
|
||||
|
||||
public void refreshChildren() {
|
||||
|
|
|
@ -176,7 +176,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
}
|
||||
} else if(newValue != null) {
|
||||
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Address freshAddress = newValue.getAddress(freshNode);
|
||||
Address freshAddress = freshNode.getAddress();
|
||||
address.setText(freshAddress.toString());
|
||||
label.requestFocus();
|
||||
}
|
||||
|
@ -326,7 +326,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
|||
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get());
|
||||
if(recipientBip47Wallet != null) {
|
||||
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
|
||||
ECKey pubKey = recipientBip47Wallet.getPubKey(sendNode);
|
||||
ECKey pubKey = sendNode.getPubKey();
|
||||
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
|
||||
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address))) {
|
||||
return address;
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
|
|||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
@ -606,9 +607,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData();
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY
|
||||
&& payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType())
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType())
|
||||
&& !(payments.get(0).getAddress() instanceof PayNymAddress)) {
|
||||
selectors.add(new StonewallUtxoSelector(noInputsFee));
|
||||
selectors.add(new StonewallUtxoSelector(payments.get(0).getAddress().getScriptType(), noInputsFee));
|
||||
}
|
||||
|
||||
selectors.addAll(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee)));
|
||||
|
@ -810,7 +811,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private void setEffectiveFeeRate(WalletTransaction walletTransaction) {
|
||||
List<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()) {
|
||||
long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum();
|
||||
double utxoTxSize = unconfirmedUtxoTxs.stream().mapToDouble(blkTx -> blkTx.getTransaction().getVirtualSize()).sum();
|
||||
|
@ -966,7 +967,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private boolean isMixPossible(List<Payment> payments) {
|
||||
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
|
||||
&& payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
}
|
||||
|
||||
private void updateOptimizationButtons(List<Payment> payments) {
|
||||
|
@ -1141,6 +1142,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
//Ensure all child wallets have been saved
|
||||
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
||||
if(!storage.isPersisted(childWallet)) {
|
||||
try {
|
||||
|
@ -1150,6 +1152,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//The WhirlpoolWallet has already been configured for the tx0 preview
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getStorage().getWalletId(masterWallet));
|
||||
|
@ -1201,8 +1204,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) {
|
||||
if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) {
|
||||
if(event.fromThisOrNested(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) {
|
||||
if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getAllHistoryChangedNodes())) {
|
||||
clear(null);
|
||||
} else {
|
||||
updateTransaction();
|
||||
|
@ -1232,7 +1235,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
if(event.fromThisOrNested(walletForm.getWallet())) {
|
||||
updateTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -1367,7 +1370,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(event.fromThisOrNested(getWalletForm().getWallet())) {
|
||||
UtxoSelector utxoSelector = utxoSelectorProperty.get();
|
||||
if(utxoSelector instanceof MaxUtxoSelector) {
|
||||
updateTransaction(true);
|
||||
|
@ -1424,12 +1427,13 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
|
||||
List<Payment> payments = walletTransaction.getPayments();
|
||||
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();
|
||||
boolean payNymPresent = isPayNymMixOnlyPayment(payments);
|
||||
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
||||
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||
if(payNymPresent) {
|
||||
|
|
|
@ -548,7 +548,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
Wallet childWallet = masterWallet.addChildWallet(entry.getKey());
|
||||
childWallet.getKeystores().clear();
|
||||
childWallet.getKeystores().add(entry.getValue());
|
||||
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
}
|
||||
saveChildWallets(masterWallet);
|
||||
}
|
||||
|
@ -556,7 +556,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
} else {
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -568,7 +568,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
} finally {
|
||||
masterWallet.encrypt(key);
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isEncrypted()) {
|
||||
if(!childWallet.isNested() && !childWallet.isEncrypted()) {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
}
|
||||
|
@ -587,7 +587,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
} else {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
}
|
||||
|
||||
saveChildWallets(masterWallet);
|
||||
|
@ -595,6 +595,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
|
||||
private void saveChildWallets(Wallet masterWallet) {
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
||||
if(!storage.isPersisted(childWallet)) {
|
||||
try {
|
||||
|
@ -606,6 +607,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setInputFieldsDisabled(boolean disabled) {
|
||||
policyType.setDisable(disabled);
|
||||
|
@ -679,7 +681,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletAdded(ChildWalletAddedEvent event) {
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
|
||||
setInputFieldsDisabled(true);
|
||||
}
|
||||
|
@ -701,7 +703,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_SET;
|
||||
}
|
||||
|
||||
if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && !walletForm.getWallet().getTransactions().isEmpty()) {
|
||||
if(!changePassword && ((SettingsWalletForm)walletForm).isAddressChange() && walletForm.getWallet().hasTransactions()) {
|
||||
Optional<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)) {
|
||||
revert.setDisable(false);
|
||||
|
@ -764,8 +766,10 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
walletForm.getStorage().setEncryptionPubKey(null);
|
||||
masterWallet.decrypt(key);
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
childWallet.decrypt(key);
|
||||
}
|
||||
}
|
||||
saveWallet(true, false);
|
||||
return;
|
||||
}
|
||||
|
@ -776,8 +780,10 @@ public class SettingsController extends WalletFormController implements Initiali
|
|||
|
||||
masterWallet.encrypt(key);
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
}
|
||||
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
|
||||
walletForm.saveAndRefresh();
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
|
|
|
@ -28,7 +28,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
|||
private final BlockTransaction blockTransaction;
|
||||
|
||||
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;
|
||||
|
||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
@ -169,7 +169,9 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
|||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
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
|
||||
|
@ -244,7 +246,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
|||
|
||||
@Subscribe
|
||||
public void blockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(getWallet().equals(event.getWallet())) {
|
||||
if(event.getWallet().equals(getWallet())) {
|
||||
setConfirmations(calculateConfirmations());
|
||||
|
||||
if(!isFullyConfirming()) {
|
||||
|
|
|
@ -182,7 +182,7 @@ public class TransactionsController extends WalletFormController implements Init
|
|||
//Will automatically update transactionsTable transactions and recalculate balances
|
||||
walletTransactionsEntry.updateTransactions();
|
||||
|
||||
transactionsTable.updateHistory(event.getHistoryChangedNodes());
|
||||
transactionsTable.updateHistory();
|
||||
balance.setValue(walletTransactionsEntry.getBalance());
|
||||
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
|
||||
balanceChart.update(walletTransactionsEntry);
|
||||
|
@ -192,7 +192,7 @@ public class TransactionsController extends WalletFormController implements Init
|
|||
|
||||
@Subscribe
|
||||
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
if(event.fromThisOrNested(walletForm.getWallet())) {
|
||||
for(Entry entry : event.getEntries()) {
|
||||
transactionsTable.updateLabel(entry);
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ public class TransactionsController extends WalletFormController implements Init
|
|||
|
||||
@Subscribe
|
||||
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
|
||||
|
|
|
@ -52,7 +52,7 @@ public class UtxoEntry extends HashIndexEntry {
|
|||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return getWallet().getAddress(node);
|
||||
return node.getAddress();
|
||||
}
|
||||
|
||||
public WalletNode getNode() {
|
||||
|
@ -60,7 +60,7 @@ public class UtxoEntry extends HashIndexEntry {
|
|||
}
|
||||
|
||||
public String getOutputDescriptor() {
|
||||
return getWallet().getOutputDescriptor(node);
|
||||
return node.getOutputDescriptor();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -283,7 +283,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
} finally {
|
||||
wallet.encrypt(key);
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(!childWallet.isEncrypted()) {
|
||||
if(!childWallet.isNested() && !childWallet.isEncrypted()) {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
}
|
||||
|
@ -340,13 +340,13 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
WalletNode premixNode = null;
|
||||
for(int i = 0; i < tx0Preview.getNbPremix(); i++) {
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -509,14 +509,14 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
}
|
||||
|
||||
updateFields(walletUtxosEntry);
|
||||
utxosTable.updateHistory(event.getHistoryChangedNodes());
|
||||
utxosTable.updateHistory();
|
||||
utxosChart.update(walletUtxosEntry);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
if(event.fromThisOrNested(walletForm.getWallet())) {
|
||||
for(Entry entry : event.getEntries()) {
|
||||
utxosTable.updateLabel(entry);
|
||||
}
|
||||
|
@ -565,7 +565,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(event.fromThisOrNested(getWalletForm().getWallet())) {
|
||||
utxosTable.refresh();
|
||||
updateButtons(Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ public class WalletForm {
|
|||
private final Storage storage;
|
||||
protected Wallet wallet;
|
||||
|
||||
private final List<WalletForm> nestedWalletForms = new ArrayList<>();
|
||||
|
||||
private WalletTransactionsEntry walletTransactionsEntry;
|
||||
private WalletUtxosEntry walletUtxosEntry;
|
||||
private final List<NodeEntry> accountEntries = new ArrayList<>();
|
||||
|
@ -85,6 +87,10 @@ public class WalletForm {
|
|||
throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet");
|
||||
}
|
||||
|
||||
public List<WalletForm> getNestedWalletForms() {
|
||||
return nestedWalletForms;
|
||||
}
|
||||
|
||||
public void revert() {
|
||||
throw new UnsupportedOperationException("Only SettingsWalletForm supports revert");
|
||||
}
|
||||
|
@ -117,10 +123,14 @@ public class WalletForm {
|
|||
}
|
||||
|
||||
public void refreshHistory(Integer blockHeight) {
|
||||
refreshHistory(blockHeight, null);
|
||||
refreshHistory(blockHeight, null, null);
|
||||
}
|
||||
|
||||
public void refreshHistory(Integer blockHeight, Set<WalletNode> nodes) {
|
||||
refreshHistory(blockHeight, null, nodes);
|
||||
}
|
||||
|
||||
public void refreshHistory(Integer blockHeight, List<Wallet> filterToWallets, Set<WalletNode> nodes) {
|
||||
Wallet previousWallet = wallet.copy();
|
||||
if(wallet.isValid() && AppServices.isConnected()) {
|
||||
if(log.isDebugEnabled()) {
|
||||
|
@ -128,12 +138,12 @@ public class WalletForm {
|
|||
}
|
||||
|
||||
Set<WalletNode> walletTransactionNodes = getWalletTransactionNodes(nodes);
|
||||
if(walletTransactionNodes == null || !walletTransactionNodes.isEmpty()) {
|
||||
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, walletTransactionNodes);
|
||||
if(!wallet.isNested() && (walletTransactionNodes == null || !walletTransactionNodes.isEmpty())) {
|
||||
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, filterToWallets, walletTransactionNodes);
|
||||
historyService.setOnSucceeded(workerStateEvent -> {
|
||||
if(historyService.getValue()) {
|
||||
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
|
||||
updateWallet(blockHeight, previousWallet);
|
||||
updateWallets(blockHeight, previousWallet);
|
||||
}
|
||||
});
|
||||
historyService.setOnFailed(workerStateEvent -> {
|
||||
|
@ -175,8 +185,8 @@ public class WalletForm {
|
|||
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new ChildWalletAddedEvent(storage, wallet, addedWallet));
|
||||
}
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, wallet, addedWallets));
|
||||
});
|
||||
paymentCodesService.setOnFailed(failedEvent -> {
|
||||
log.error("Could not determine payment codes for wallet " + wallet.getFullName(), failedEvent.getSource().getException());
|
||||
|
@ -186,33 +196,51 @@ public class WalletForm {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateWallet(Integer blockHeight, Wallet previousWallet) {
|
||||
private void updateWallets(Integer blockHeight, Wallet previousWallet) {
|
||||
List<WalletNode> nestedHistoryChangedNodes = new ArrayList<>();
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
Wallet previousChildWallet = previousWallet.getChildWallet(childWallet.getName());
|
||||
if(previousChildWallet != null) {
|
||||
nestedHistoryChangedNodes.addAll(updateWallet(blockHeight, childWallet, previousChildWallet, Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateWallet(blockHeight, wallet, previousWallet, nestedHistoryChangedNodes);
|
||||
}
|
||||
|
||||
private List<WalletNode> updateWallet(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List<WalletNode> nestedHistoryChangedNodes) {
|
||||
if(blockHeight != null) {
|
||||
wallet.setStoredBlockHeight(blockHeight);
|
||||
currentWallet.setStoredBlockHeight(blockHeight);
|
||||
}
|
||||
|
||||
notifyIfChanged(blockHeight, previousWallet);
|
||||
return notifyIfChanged(blockHeight, currentWallet, previousWallet, nestedHistoryChangedNodes);
|
||||
}
|
||||
|
||||
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) {
|
||||
private List<WalletNode> notifyIfChanged(Integer blockHeight, Wallet currentWallet, Wallet previousWallet, List<WalletNode> nestedHistoryChangedNodes) {
|
||||
List<WalletNode> historyChangedNodes = new ArrayList<>();
|
||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
|
||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren()));
|
||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), currentWallet.getNode(KeyPurpose.RECEIVE).getChildren()));
|
||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), currentWallet.getNode(KeyPurpose.CHANGE).getChildren()));
|
||||
|
||||
boolean changed = false;
|
||||
if(!historyChangedNodes.isEmpty() || !nestedHistoryChangedNodes.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(currentWallet, storage, historyChangedNodes, nestedHistoryChangedNodes)));
|
||||
if(!historyChangedNodes.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(blockHeight != null && !blockHeight.equals(previousWallet.getStoredBlockHeight())) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(wallet, blockHeight)));
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(currentWallet, blockHeight)));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if(changed) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(currentWallet)));
|
||||
}
|
||||
|
||||
return historyChangedNodes;
|
||||
}
|
||||
|
||||
private List<WalletNode> getHistoryChangedNodes(Set<WalletNode> previousNodes, Set<WalletNode> currentNodes) {
|
||||
|
@ -390,7 +418,7 @@ public class WalletForm {
|
|||
public void newBlock(NewBlockEvent event) {
|
||||
//Check if wallet is valid to avoid saving wallets in initial setup
|
||||
if(wallet.isValid()) {
|
||||
updateWallet(event.getHeight(), wallet.copy());
|
||||
updateWallet(event.getHeight(), wallet, wallet.copy(), Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -401,7 +429,7 @@ public class WalletForm {
|
|||
|
||||
@Subscribe
|
||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||
if(wallet.isValid()) {
|
||||
if(wallet.isValid() && !wallet.isNested()) {
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
@ -443,7 +471,7 @@ public class WalletForm {
|
|||
|
||||
@Subscribe
|
||||
public void walletLabelsChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
if(event.toThisOrNested(wallet)) {
|
||||
Map<Entry, Entry> labelChangedEntries = new LinkedHashMap<>();
|
||||
for(Entry entry : event.getEntries()) {
|
||||
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
|
||||
|
@ -456,8 +484,7 @@ public class WalletForm {
|
|||
receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry);
|
||||
}
|
||||
//Avoid recursive changes to address labels - only initial transaction label changes can change address labels
|
||||
if((childNode.getLabel() == null || childNode.getLabel().isEmpty()) && event.getSource(entry) == null) {
|
||||
if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) {
|
||||
childNode.setLabel(entry.getLabel());
|
||||
labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry);
|
||||
}
|
||||
|
@ -481,7 +508,8 @@ public class WalletForm {
|
|||
}
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
BlockTransaction blockTransaction = hashIndexEntry.getBlockTransaction();
|
||||
if(blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) {
|
||||
//Avoid recursive changes from hashIndexEntries
|
||||
if((blockTransaction.getLabel() == null || blockTransaction.getLabel().isEmpty()) && event.getSource(entry) == null) {
|
||||
blockTransaction.setLabel(entry.getLabel());
|
||||
labelChangedEntries.put(new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()), entry);
|
||||
}
|
||||
|
@ -589,6 +617,9 @@ public class WalletForm {
|
|||
AppServices.clearTransactionHistoryCache(wallet);
|
||||
}
|
||||
EventManager.get().unregister(this);
|
||||
for(WalletForm nestedWalletForm : nestedWalletForms) {
|
||||
EventManager.get().unregister(nestedWalletForm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -598,4 +629,14 @@ public class WalletForm {
|
|||
accountEntries.clear();
|
||||
EventManager.get().post(new WalletAddressesStatusEvent(wallet));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
List<Wallet> nestedWallets = event.getChildWallets().stream().filter(Wallet::isNested).collect(Collectors.toList());
|
||||
if(!nestedWallets.isEmpty()) {
|
||||
Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), nestedWallets, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,9 @@ public class WalletTransactionsEntry extends Entry {
|
|||
}
|
||||
|
||||
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> previous = new ArrayList<>(getChildren());
|
||||
|
||||
|
@ -80,8 +83,6 @@ public class WalletTransactionsEntry extends Entry {
|
|||
|
||||
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());
|
||||
if(!entriesComplete.isEmpty()) {
|
||||
EventManager.get().post(new NewWalletTransactionsEvent(getWallet(), entriesAdded.stream().map(entry -> (TransactionEntry)entry).collect(Collectors.toList())));
|
||||
|
@ -104,6 +105,14 @@ public class WalletTransactionsEntry extends Entry {
|
|||
getWalletTransactions(wallet, walletTransactionMap, wallet.getNode(keyPurpose));
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
||||
getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletTransaction> walletTransactions = new ArrayList<>(walletTransactionMap.values());
|
||||
Collections.sort(walletTransactions);
|
||||
return walletTransactions;
|
||||
|
@ -114,7 +123,7 @@ public class WalletTransactionsEntry extends Entry {
|
|||
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
||||
for(WalletNode addressNode : childNodes) {
|
||||
for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) {
|
||||
BlockTransaction inputTx = wallet.getTransactions().get(hashIndex.getHash());
|
||||
BlockTransaction inputTx = wallet.getWalletTransaction(hashIndex.getHash());
|
||||
//A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again
|
||||
if(inputTx != null) {
|
||||
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
|
||||
|
@ -125,7 +134,7 @@ public class WalletTransactionsEntry extends Entry {
|
|||
inputWalletTx.incoming.put(hashIndex, keyPurpose);
|
||||
|
||||
if(hashIndex.getSpentBy() != null) {
|
||||
BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash());
|
||||
BlockTransaction outputTx = wallet.getWalletTransaction(hashIndex.getSpentBy().getHash());
|
||||
if(outputTx != null) {
|
||||
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
|
||||
if(outputWalletTx == null) {
|
||||
|
|
|
@ -10,7 +10,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class WalletUtxosEntry extends Entry {
|
||||
public WalletUtxosEntry(Wallet wallet) {
|
||||
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||
calculateDuplicates();
|
||||
updateMixProgress();
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ public class WalletUtxosEntry extends Entry {
|
|||
}
|
||||
|
||||
public void updateUtxos() {
|
||||
List<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> entriesAdded = new ArrayList<>(current);
|
||||
|
|
|
@ -237,7 +237,7 @@ public class Whirlpool {
|
|||
}
|
||||
|
||||
public MixProgress getMixProgress(BlockTransactionHashIndex utxo) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null || utxo.getStatus() == Status.FROZEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -409,7 +409,7 @@ public class Whirlpool {
|
|||
return StandardAccount.ACCOUNT_0;
|
||||
}
|
||||
|
||||
public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
public static UnspentOutput getUnspentOutput(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
|
||||
|
||||
UnspentOutput out = new UnspentOutput();
|
||||
|
@ -431,6 +431,7 @@ public class Whirlpool {
|
|||
out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
Wallet wallet = node.getWallet().isBip47() ? node.getWallet().getMasterWallet() : node.getWallet();
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores");
|
||||
}
|
||||
|
@ -558,7 +559,7 @@ public class Whirlpool {
|
|||
|
||||
private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) {
|
||||
for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) {
|
||||
if(walletUtxo.wallet.getAddress(walletNode).toString().equals(e.getMixProgress().getDestination().getAddress())) {
|
||||
if(walletNode.getAddress().toString().equals(e.getMixProgress().getDestination().getAddress())) {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
|
@ -638,12 +639,10 @@ public class Whirlpool {
|
|||
|
||||
public static class Tx0PreviewsService extends Service<Tx0Previews> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final Wallet wallet;
|
||||
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.wallet = wallet;
|
||||
this.utxoEntries = utxoEntries;
|
||||
}
|
||||
|
||||
|
@ -654,7 +653,7 @@ public class Whirlpool {
|
|||
updateProgress(-1, 1);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -318,7 +318,7 @@ public class WhirlpoolController {
|
|||
whirlpool.setScode(mixConfig.getScode());
|
||||
whirlpool.setTx0FeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue()));
|
||||
|
||||
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, wallet, utxoEntries);
|
||||
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, utxoEntries);
|
||||
tx0PreviewsService.setOnRunning(workerStateEvent -> {
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputsLoading.setText("Calculating...");
|
||||
|
|
|
@ -12,9 +12,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.TorService;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
|
@ -182,7 +180,7 @@ public class WhirlpoolServices {
|
|||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
EventManager.get().post(new ChildWalletAddedEvent(storage, decryptedWallet, childWallet));
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.wallet.api.backend.MinerFee;
|
||||
import com.samourai.wallet.api.backend.MinerFeeTarget;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.api.backend.beans.WalletResponse;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0ParamService;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo;
|
||||
import com.samourai.whirlpool.client.wallet.data.chain.ChainSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.WalletResponseDataSource;
|
||||
import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.pool.PoolSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.wallet.WalletSupplier;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
|
@ -79,8 +86,9 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
continue;
|
||||
}
|
||||
|
||||
allTransactions.putAll(wallet.getTransactions());
|
||||
wallet.getTransactions().keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub));
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
|
||||
allTransactions.putAll(walletTransactions);
|
||||
walletTransactions.keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub));
|
||||
if(wallet.getStoredBlockHeight() != null) {
|
||||
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight());
|
||||
}
|
||||
|
@ -93,13 +101,13 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
address.account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex;
|
||||
int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
|
||||
address.change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex;
|
||||
address.n_tx = wallet.getTransactions().size();
|
||||
address.n_tx = walletTransactions.size();
|
||||
addresses.add(address);
|
||||
|
||||
for(Map.Entry<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) {
|
||||
unspentOutputs.add(Whirlpool.getUnspentOutput(wallet, utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
||||
unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,6 +172,50 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
return walletResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BasicUtxoSupplier computeUtxoSupplier(WhirlpoolWallet whirlpoolWallet, WalletSupplier walletSupplier, UtxoConfigSupplier utxoConfigSupplier, ChainSupplier chainSupplier, PoolSupplier poolSupplier, Tx0ParamService tx0ParamService) throws Exception {
|
||||
return new BasicUtxoSupplier(
|
||||
walletSupplier,
|
||||
utxoConfigSupplier,
|
||||
chainSupplier,
|
||||
poolSupplier,
|
||||
tx0ParamService) {
|
||||
@Override
|
||||
public void refresh() throws Exception {
|
||||
SparrowDataSource.this.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUtxoChanges(UtxoData utxoData) {
|
||||
super.onUtxoChanges(utxoData);
|
||||
whirlpoolWallet.onUtxoChanges(utxoData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] _getPrivKeyBytes(WhirlpoolUtxo whirlpoolUtxo) {
|
||||
UnspentOutput utxo = whirlpoolUtxo.getUtxo();
|
||||
Wallet wallet = getWallet(utxo.xpub.m);
|
||||
Map<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
|
||||
public void pushTx(String txHex) throws Exception {
|
||||
Transaction transaction = new Transaction(Utils.hexToBytes(txHex));
|
||||
|
|
|
@ -39,7 +39,8 @@ public class SparrowPostmixHandler implements IPostmixHandler {
|
|||
int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex);
|
||||
|
||||
// address
|
||||
Address address = wallet.getAddress(new WalletNode(keyPurpose, index));
|
||||
WalletNode node = new WalletNode(wallet, keyPurpose, index);
|
||||
Address address = node.getAddress();
|
||||
String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num());
|
||||
|
||||
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);
|
||||
|
|
|
@ -52,7 +52,7 @@ public class StorageTest extends IoTest {
|
|||
Assert.assertEquals("xpub6BrhGFTWPd3DXo8s2BPxHHzCmBCyj8QvamcEUaq8EDwnwXpvvcU9LzpJqENHcqHkqwTn2vPhynGVoEqj3PAB3NxnYZrvCsSfoCniJKaggdy", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
|
||||
Assert.assertEquals("af6ebd81714c301c3a71fe11a7a9c99ccef4b33d4b36582220767bfa92768a2aa040f88b015b2465f8075a8b9dbf892a7d6e6c49932109f2cbc05ba0bd7f355fbcc34c237f71be5fb4dd7f8184e44cb0", Utils.bytesToHex(wallet.getKeystores().get(0).getSeed().getEncryptedData().getEncryptedBytes()));
|
||||
Assert.assertNull(wallet.getKeystores().get(0).getSeed().getMnemonicCode());
|
||||
Assert.assertEquals("bc1q2mkrttcuzryrdyn9vtu3nfnt3jlngwn476ktus", wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||
Assert.assertEquals("bc1q2mkrttcuzryrdyn9vtu3nfnt3jlngwn476ktus", wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue