mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
terminal - add mix selected functionality to broadcast premix transactions
This commit is contained in:
parent
b7992ae9e1
commit
871c503bc9
9 changed files with 584 additions and 88 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<>();
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -480,6 +480,10 @@ public class Whirlpool {
|
|||
config.setScode(scode);
|
||||
}
|
||||
|
||||
public Tx0FeeTarget getTx0FeeTarget() {
|
||||
return tx0FeeTarget;
|
||||
}
|
||||
|
||||
public void setTx0FeeTarget(Tx0FeeTarget tx0FeeTarget) {
|
||||
this.tx0FeeTarget = tx0FeeTarget;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue