terminal - add mix selected functionality to broadcast premix transactions

This commit is contained in:
Craig Raw 2022-11-08 10:09:25 +02:00
parent b7992ae9e1
commit 871c503bc9
9 changed files with 584 additions and 88 deletions

View file

@ -0,0 +1,124 @@
package com.sparrowwallet.sparrow.terminal.wallet;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
import java.util.List;
import java.util.Locale;
public class MixDialog extends WalletDialog {
private static final List<FeePriority> FEE_PRIORITIES = List.of(new FeePriority("Low", Tx0FeeTarget.MIN), new FeePriority("Normal", Tx0FeeTarget.BLOCKS_4), new FeePriority("High", Tx0FeeTarget.BLOCKS_2));
private final String walletId;
private final List<UtxoEntry> utxoEntries;
private final TextBox scode;
private final ComboBox<FeePriority> premixPriority;
private final Label premixFeeRate;
private Pool mixPool;
public MixDialog(String walletId, WalletForm walletForm, List<UtxoEntry> utxoEntries) {
super(walletForm.getWallet().getFullDisplayName() + " Premix Config", walletForm);
this.walletId = walletId;
this.utxoEntries = utxoEntries;
setHints(List.of(Hint.CENTERED));
Wallet wallet = walletForm.getWallet();
MixConfig mixConfig = wallet.getMasterMixConfig();
Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(new Label("SCODE"));
scode = new TextBox(new TerminalSize(20, 1));
mainPanel.addComponent(scode);
mainPanel.addComponent(new Label("Premix priority"));
premixPriority = new ComboBox<>();
FEE_PRIORITIES.forEach(premixPriority::addItem);
mainPanel.addComponent(premixPriority);
mainPanel.addComponent(new Label("Premix fee rate"));
premixFeeRate = new Label("");
mainPanel.addComponent(premixFeeRate);
Panel buttonPanel = new Panel();
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
Button next = new Button("Next", this::onNext).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
buttonPanel.addComponent(next);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
setComponent(mainPanel);
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
premixPriority.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
FeePriority feePriority = premixPriority.getItem(selectedIndex);
premixFeeRate.setText(SparrowMinerFeeSupplier.getFee(Integer.parseInt(feePriority.getTx0FeeTarget().getFeeTarget().getValue())) + " sats/vB");
});
premixPriority.setSelectedIndex(1);
scode.setTextChangeListener((newText, changedByUserInteraction) -> {
if(changedByUserInteraction) {
scode.setText(newText.toUpperCase(Locale.ROOT));
}
mixConfig.setScode(newText.toUpperCase(Locale.ROOT));
EventManager.get().post(new WalletMasterMixConfigChangedEvent(wallet));
});
}
private void onNext() {
MixPoolDialog mixPoolDialog = new MixPoolDialog(walletId, getWalletForm(), utxoEntries, premixPriority.getSelectedItem().getTx0FeeTarget());
mixPool = mixPoolDialog.showDialog(SparrowTerminal.get().getGui());
close();
}
private void onCancel() {
close();
}
@Override
public Pool showDialog(WindowBasedTextGUI textGUI) {
super.showDialog(textGUI);
return mixPool;
}
private static class FeePriority {
private final String name;
private final Tx0FeeTarget tx0FeeTarget;
public FeePriority(String name, Tx0FeeTarget tx0FeeTarget) {
this.name = name;
this.tx0FeeTarget = tx0FeeTarget;
}
public Tx0FeeTarget getTx0FeeTarget() {
return tx0FeeTarget;
}
@Override
public String toString() {
return name;
}
}
}

View file

@ -0,0 +1,240 @@
package com.sparrowwallet.sparrow.terminal.wallet;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.samourai.whirlpool.client.tx0.Tx0Preview;
import com.samourai.whirlpool.client.tx0.Tx0Previews;
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
public class MixPoolDialog extends WalletDialog {
private static final DisplayPool NULL_POOL = new DisplayPool(null);
private final String walletId;
private final List<UtxoEntry> utxoEntries;
private final Tx0FeeTarget tx0FeeTarget;
private final ComboBox<DisplayPool> pool;
private final Label poolFeeLabel;
private final Label poolFee;
private final Label premixOutputs;
private final Button broadcast;
private Tx0Previews tx0Previews;
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null);
private Pool mixPool;
public MixPoolDialog(String walletId, WalletForm walletForm, List<UtxoEntry> utxoEntries, Tx0FeeTarget tx0FeeTarget) {
super(walletForm.getWallet().getFullDisplayName() + " Premix Pool", walletForm);
this.walletId = walletId;
this.utxoEntries = utxoEntries;
this.tx0FeeTarget = tx0FeeTarget;
setHints(List.of(Hint.CENTERED));
Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(new Label("Pool"));
pool = new ComboBox<>();
pool.addItem(NULL_POOL);
pool.setEnabled(false);
mainPanel.addComponent(pool);
poolFeeLabel = new Label("Pool fee");
poolFeeLabel.setPreferredSize(new TerminalSize(21, 1));
mainPanel.addComponent(poolFeeLabel);
poolFee = new Label("");
mainPanel.addComponent(poolFee);
mainPanel.addComponent(new Label("Premix outputs"));
premixOutputs = new Label("");
mainPanel.addComponent(premixOutputs);
Panel buttonPanel = new Panel();
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
broadcast = new Button("Broadcast", this::onBroadcast).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
buttonPanel.addComponent(broadcast);
broadcast.setEnabled(false);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
setComponent(mainPanel);
pool.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
DisplayPool selectedPool = pool.getSelectedItem();
if(selectedPool != NULL_POOL) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
poolFee.setText(format.formatSatsValue(selectedPool.pool.getFeeValue()) + " sats");
fetchTx0Preview(selectedPool.pool);
}
});
tx0PreviewProperty.addListener((observable, oldValue, tx0Preview) -> {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
if(tx0Preview == null) {
premixOutputs.setText("Calculating...");
broadcast.setEnabled(false);
} else {
if(tx0Preview.getPool().getFeeValue() != tx0Preview.getTx0Data().getFeeValue()) {
poolFeeLabel.setText("Pool fee (discounted)");
} else {
poolFeeLabel.setText("Pool fee");
}
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
poolFee.setText(format.formatSatsValue(tx0Preview.getTx0Data().getFeeValue()) + " sats");
premixOutputs.setText(tx0Preview.getNbPremix() + " UTXOs");
broadcast.setEnabled(true);
}
});
});
Platform.runLater(this::fetchPools);
}
private void onBroadcast() {
mixPool = tx0PreviewProperty.get() == null ? null : tx0PreviewProperty.get().getPool();
close();
}
private void onCancel() {
close();
}
@Override
public Pool showDialog(WindowBasedTextGUI textGUI) {
super.showDialog(textGUI);
return mixPool;
}
private void fetchPools() {
long totalUtxoValue = utxoEntries.stream().mapToLong(Entry::getValue).sum();
Whirlpool.PoolsService poolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), totalUtxoValue);
poolsService.setOnSucceeded(workerStateEvent -> {
List<Pool> availablePools = poolsService.getValue().stream().toList();
if(availablePools.isEmpty()) {
SparrowTerminal.get().getGuiThread().invokeLater(() -> pool.setEnabled(false));
Whirlpool.PoolsService allPoolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), null);
allPoolsService.setOnSucceeded(poolsStateEvent -> {
OptionalLong optMinValue = allPoolsService.getValue().stream().mapToLong(pool1 -> pool1.getPremixValueMin() + pool1.getFeeValue()).min();
if(optMinValue.isPresent() && totalUtxoValue < optMinValue.getAsLong()) {
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
String satsValue = format.formatSatsValue(optMinValue.getAsLong()) + " sats";
String btcValue = format.formatBtcValue(optMinValue.getAsLong()) + " BTC";
AppServices.showErrorDialog("Insufficient UTXO Value", "No available pools. Select a value over " + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + ".");
SparrowTerminal.get().getGuiThread().invokeLater(this::close);
}
});
allPoolsService.start();
} else {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
pool.setEnabled(true);
pool.clearItems();
availablePools.stream().map(DisplayPool::new).forEach(pool::addItem);
pool.setSelectedIndex(0);
});
}
});
poolsService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
while(exception.getCause() != null) {
exception = exception.getCause();
}
Optional<ButtonType> optButton = AppServices.showErrorDialog("Error fetching pools", exception.getMessage(), ButtonType.CANCEL, new ButtonType("Retry", ButtonBar.ButtonData.APPLY));
if(optButton.isPresent()) {
if(optButton.get().getButtonData().equals(ButtonBar.ButtonData.APPLY)) {
fetchPools();
} else {
SparrowTerminal.get().getGuiThread().invokeLater(() -> pool.setEnabled(false));
}
}
});
poolsService.start();
}
private void fetchTx0Preview(Pool pool) {
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
if(mixConfig.getScode() == null) {
mixConfig.setScode("");
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
}
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
if(tx0Previews != null && mixConfig.getScode().equals(whirlpool.getScode()) && tx0FeeTarget == whirlpool.getTx0FeeTarget()) {
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId());
tx0PreviewProperty.set(tx0Preview);
} else {
tx0Previews = null;
whirlpool.setScode(mixConfig.getScode());
whirlpool.setTx0FeeTarget(tx0FeeTarget);
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, utxoEntries);
tx0PreviewsService.setOnRunning(workerStateEvent -> {
premixOutputs.setText("Calculating...");
tx0PreviewProperty.set(null);
});
tx0PreviewsService.setOnSucceeded(workerStateEvent -> {
tx0Previews = tx0PreviewsService.getValue();
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId());
tx0PreviewProperty.set(tx0Preview);
});
tx0PreviewsService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
while(exception.getCause() != null) {
exception = exception.getCause();
}
AppServices.showErrorDialog("Error fetching Tx0","Error fetching Tx0: " + exception.getMessage());
});
tx0PreviewsService.start();
}
}
private static final class DisplayPool {
private final Pool pool;
public DisplayPool(Pool pool) {
this.pool = pool;
}
@Override
public String toString() {
if(pool == null) {
return "Fetching pools...";
}
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
return format.formatSatsValue(pool.getDenomination()) + " sats";
}
}
}

View file

@ -111,7 +111,15 @@ public class SettingsDialog extends WalletDialog {
StandardAccount standardAccount = addAccountDialog.showDialog(SparrowTerminal.get().getGui());
if(standardAccount != null) {
addAccount(masterWallet, standardAccount);
addAccount(masterWallet, standardAccount, () -> {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
showSuccessDialog("Added Accounts", "Whirlpool Accounts have been successfully added.");
} else {
showSuccessDialog("Added Account", standardAccount.getName() + " has been successfully added.");
}
});
});
}
}
@ -255,89 +263,6 @@ public class SettingsDialog extends WalletDialog {
}
}
private void addAccount(Wallet masterWallet, StandardAccount standardAccount) {
if(masterWallet.isEncrypted()) {
String walletId = getWalletForm().getWalletId();
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password");
builder.setDescription("Enter the wallet password:");
builder.setPasswordInput(true);
String password = builder.build().showDialog(SparrowTerminal.get().getGui());
if(password != null) {
Platform.runLater(() -> {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(getWalletForm().getStorage(), new SecureString(password), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), getWalletForm().getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
encryptionFullKey.clear();
masterWallet.decrypt(key);
addAndEncryptAccount(masterWallet, standardAccount, key);
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
showErrorDialog("Invalid Password", "The wallet password was invalid.");
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
});
}
} else {
Platform.runLater(() -> addAndSaveAccount(masterWallet, standardAccount, null));
}
}
private void addAndEncryptAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
try {
addAndSaveAccount(masterWallet, standardAccount, key);
} finally {
masterWallet.encrypt(key);
key.clear();
}
}
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
List<Wallet> childWallets;
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
childWallets = WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
SparrowTerminal.get().getGuiThread().invokeLater(() -> showSuccessDialog("Added Accounts", "Whirlpool Accounts have been successfully added."));
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
childWallets = List.of(childWallet);
SparrowTerminal.get().getGuiThread().invokeLater(() -> showSuccessDialog("Added Account", standardAccount.getName() + " has been successfully added."));
}
if(key != null) {
for(Wallet childWallet : childWallets) {
childWallet.encrypt(key);
}
}
saveChildWallets(masterWallet);
}
private void saveChildWallets(Wallet masterWallet) {
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isNested()) {
Storage storage = getWalletForm().getStorage();
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
} catch(Exception e) {
log.error("Error saving wallet", e);
showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
}
}
}
}
public static List<String> splitString(String stringToSplit, int maxLength) {
String text = stringToSplit;
List<String> lines = new ArrayList<>();

View file

@ -6,16 +6,19 @@ import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.table.Table;
import com.googlecode.lanterna.gui2.table.TableModel;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.terminal.ModalDialog;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.terminal.wallet.table.*;
import com.sparrowwallet.sparrow.wallet.*;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
@ -23,6 +26,7 @@ import javafx.beans.value.WeakChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
public class UtxosDialog extends WalletDialog {
private final Label balance;
@ -34,6 +38,7 @@ public class UtxosDialog extends WalletDialog {
private Button startMix;
private Button mixTo;
private Button mixSelected;
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
SparrowTerminal.get().getGuiThread().invokeLater(() -> startMix.setEnabled(newValue));
@ -75,6 +80,8 @@ public class UtxosDialog extends WalletDialog {
}
}
}
startMix.setLabel(newValue ? "Stop Mixing" : "Start Mixing");
};
public UtxosDialog(WalletForm walletForm) {
@ -99,6 +106,13 @@ public class UtxosDialog extends WalletDialog {
utxos = new Table<>(getTableColumns());
utxos.setTableCellRenderer(new EntryTableCellRenderer());
utxos.setSelectAction(() -> {
if(utxos.getTableModel().getRowCount() > utxos.getSelectedRow()) {
TableCell dateCell = utxos.getTableModel().getRow(utxos.getSelectedRow()).get(0);
dateCell.setSelected(!dateCell.isSelected());
updateMixSelectedButton();
}
});
updateLabels(walletUtxosEntry);
updateHistory(getWalletForm().getWalletUtxosEntry());
@ -136,7 +150,14 @@ public class UtxosDialog extends WalletDialog {
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
} else {
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
if(WhirlpoolServices.canWalletMix(getWalletForm().getWallet())) {
mixSelected = new Button("Mix Selected", this::mixSelected);
mixSelected.setEnabled(false);
buttonPanel.addComponent(mixSelected);
} else {
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
}
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
@ -306,6 +327,61 @@ public class UtxosDialog extends WalletDialog {
}
}
private void updateMixSelectedButton() {
if(mixSelected == null) {
return;
}
mixSelected.setEnabled(!getSelectedEntries().isEmpty());
}
private List<UtxoEntry> getSelectedEntries() {
return utxos.getTableModel().getRows().stream().map(row -> row.get(0)).filter(TableCell::isSelected).map(dateCell -> (UtxoEntry)dateCell.getEntry()).collect(Collectors.toList());
}
private void mixSelected() {
MixDialog mixDialog = new MixDialog(getWalletForm().getMasterWalletId(), getWalletForm(), getSelectedEntries());
Pool pool = mixDialog.showDialog(SparrowTerminal.get().getGui());
if(pool != null) {
Wallet wallet = getWalletForm().getWallet();
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet()) {
addAccount(wallet, StandardAccount.WHIRLPOOL_PREMIX, () -> broadcastPremix(pool));
} else {
Platform.runLater(() -> broadcastPremix(pool));
}
}
}
public void broadcastPremix(Pool pool) {
ModalDialog broadcastingDialog = new ModalDialog(getWalletForm().getWallet().getFullDisplayName(), "Broadcasting premix...");
SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().addWindow(broadcastingDialog));
//The WhirlpoolWallet has already been configured
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getStorage().getWalletId(getWalletForm().getMasterWallet()));
List<BlockTransactionHashIndex> utxos = getSelectedEntries().stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, pool, utxos);
tx0BroadcastService.setOnSucceeded(workerStateEvent -> {
Sha256Hash txid = tx0BroadcastService.getValue();
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
SparrowTerminal.get().getGui().removeWindow(broadcastingDialog);
AppServices.showSuccessDialog("Broadcast Successful", "Premix transaction id:\n" + txid.toString());
});
});
tx0BroadcastService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
while(exception.getCause() != null) {
exception = exception.getCause();
}
String message = exception.getMessage();
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
SparrowTerminal.get().getGui().removeWindow(broadcastingDialog);
AppServices.showErrorDialog("Error broadcasting premix transaction", message);
});
});
tx0BroadcastService.start();
}
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet())) {

View file

@ -2,23 +2,42 @@ package com.sparrowwallet.sparrow.terminal.wallet;
import com.google.common.base.Strings;
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.event.ChildWalletsAddedEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.wallet.Function;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Currency;
import java.util.List;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class WalletDialog extends DialogWindow {
private static final Logger log = LoggerFactory.getLogger(WalletDialog.class);
private final WalletForm walletForm;
public WalletDialog(String title, WalletForm walletForm) {
@ -52,6 +71,95 @@ public class WalletDialog extends DialogWindow {
}
}
protected void addAccount(Wallet masterWallet, StandardAccount standardAccount, Runnable postAddition) {
if(masterWallet.isEncrypted()) {
String walletId = getWalletForm().getWalletId();
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password");
builder.setDescription("Enter the wallet password:");
builder.setPasswordInput(true);
String password = builder.build().showDialog(SparrowTerminal.get().getGui());
if(password != null) {
Platform.runLater(() -> {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(getWalletForm().getStorage(), new SecureString(password), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), getWalletForm().getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
encryptionFullKey.clear();
masterWallet.decrypt(key);
addAndEncryptAccount(masterWallet, standardAccount, key);
if(postAddition != null) {
postAddition.run();
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
showErrorDialog("Invalid Password", "The wallet password was invalid.");
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
});
}
} else {
Platform.runLater(() -> {
addAndSaveAccount(masterWallet, standardAccount, null);
if(postAddition != null) {
postAddition.run();
}
});
}
}
private void addAndEncryptAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
try {
addAndSaveAccount(masterWallet, standardAccount, key);
} finally {
masterWallet.encrypt(key);
key.clear();
}
}
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
List<Wallet> childWallets;
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
childWallets = WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
childWallets = List.of(childWallet);
}
if(key != null) {
for(Wallet childWallet : childWallets) {
childWallet.encrypt(key);
}
}
saveChildWallets(masterWallet);
}
private void saveChildWallets(Wallet masterWallet) {
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isNested()) {
Storage storage = getWalletForm().getStorage();
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
} catch(Exception e) {
log.error("Error saving wallet", e);
showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
}
}
}
}
protected String formatBitcoinValue(long value, boolean appendUnit) {
BitcoinUnit unit = Config.get().getBitcoinUnit();
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {

View file

@ -18,6 +18,16 @@ public class DateTableCell extends TableCell {
@Override
public String formatCell() {
String unselected = formatUnselectedCell();
if(selected) {
return "(*) " + unselected.substring(Math.min(4, unselected.length()));
}
return unselected;
}
public String formatUnselectedCell() {
if(entry instanceof TransactionEntry transactionEntry && transactionEntry.getBlockTransaction() != null) {
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
return "Unconfirmed Parent";

View file

@ -4,6 +4,7 @@ import com.sparrowwallet.sparrow.wallet.Entry;
public abstract class TableCell {
protected final Entry entry;
protected boolean selected;
public TableCell(Entry entry) {
this.entry = entry;
@ -14,4 +15,12 @@ public abstract class TableCell {
}
public abstract String formatCell();
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
}

View file

@ -171,7 +171,7 @@ public class UtxoEntry extends HashIndexEntry {
}
public UtxoMixData getUtxoMixData() {
Wallet wallet = getUtxoEntry().getWallet().getMasterWallet();
Wallet wallet = getUtxoEntry().getWallet().isMasterWallet() ? getUtxoEntry().getWallet() : getUtxoEntry().getWallet().getMasterWallet();
if(wallet.getUtxoMixData(getHashIndex()) != null) {
return wallet.getUtxoMixData(getHashIndex());
}

View file

@ -480,6 +480,10 @@ public class Whirlpool {
config.setScode(scode);
}
public Tx0FeeTarget getTx0FeeTarget() {
return tx0FeeTarget;
}
public void setTx0FeeTarget(Tx0FeeTarget tx0FeeTarget) {
this.tx0FeeTarget = tx0FeeTarget;
}