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"; |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue