Code cleanup & refactoring

This commit is contained in:
QcMrHyde 2025-06-03 00:18:09 -04:00
parent 550df1d91a
commit 435f126933
4 changed files with 255 additions and 118 deletions

View file

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.joinstr;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
@ -13,45 +12,33 @@ import com.sparrowwallet.drongo.wallet.InsufficientFundsException;
import com.sparrowwallet.drongo.wallet.KnapsackUtxoSelector;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.SpentTxoFilter;
import com.sparrowwallet.drongo.wallet.StonewallUtxoSelector;
import com.sparrowwallet.drongo.wallet.TxoFilter;
import com.sparrowwallet.drongo.wallet.UtxoSelector;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.QRScanDialog;
import com.sparrowwallet.sparrow.io.WalletTransactions;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.Tor;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
import java.util.*;
public class JoinstrPool {
private final String relay;
private final Integer port;
private final Integer port; // Nostr Port ?
private final String pubkey;
private final long denomination; // Should be in sats, like transaction amounts
private final WalletForm walletForm; // [FormController].getWalletForm()
public JoinstrPool(WalletForm walletForm,String relay, Integer port, String pubkey, long denomination) {
public JoinstrPool(Integer port, String pubkey, long denomination) {
this.walletForm = walletForm;
this.relay = relay;
this.port = port;
this.pubkey = pubkey;
this.denomination = denomination;
}
public String getRelay() {
return relay;
private String getNostrRelay() {
return Config.get().getNostrRelay();
}
public Integer getPort() {
@ -73,78 +60,13 @@ public class JoinstrPool {
}
private Address getNewSendAddress() {
NodeEntry freshNodeEntry = walletForm.getFreshNodeEntry(KeyPurpose.SEND, null);
return freshNodeEntry.getAddress();
public void publicNostrEvent() {
// TODO: Publish a nostr event with pool info
}
public void createPSBT() throws InsufficientFundsException {
/* PSBT from byte[]
if(PSBT.isPSBT(bytes)) {
//Don't verify signatures here - provided PSBT may omit UTXO data that can be found when combining with an existing PSBT
PSBT psbt = new PSBT(bytes, false);
} else {
throw new ParseException("Not a valid PSBT or transaction", 0);
public void waitForPeers() {
// TODO: Wait for others to join
}
*/
/* PSBT from QR Code
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(rootStack.getScene().getWindow());
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
QRScanDialog.Result result = optionalResult.get();
result.psbt;
*/
// PSBT from WalletTransaction
Address sendAddress = getNewSendAddress();
Wallet wallet = walletForm.getWallet();
double feeRate = 1.0;
double longTermFeeRate = 10.0;
long fee = 10L;
long dustThreshold = getRecipientDustThreshold(sendAddress);
long amount = wallet.getWalletTxos().keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
long satsLeft = amount;
String paymentLabel = "coinjoin";
ArrayList<Payment> payments = new ArrayList<Payment>();
while(satsLeft > denomination + dustThreshold) {
Payment payment = new Payment(sendAddress, paymentLabel, denomination, false);
satsLeft -= denomination;
payment.setType(Payment.Type.COINJOIN);
payments.add(payment);
}
List<UtxoSelector> selectors = new ArrayList<>();
long noInputsFee = wallet.getNoInputsFee(payments, feeRate);
long costOfChange = wallet.getCostOfChange(feeRate, longTermFeeRate);
selectors.addAll(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee)));
SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null);
List<TxoFilter> txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(walletForm.getWallet()));
ArrayList<byte[]> opReturns = new ArrayList<>();
TreeSet<WalletNode> excludedChangeNodes = new TreeSet<>();
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
boolean groupByAddress = false;
boolean includeMempoolOutputs = false;
WalletTransaction walletTransaction = walletForm.getWallet().createWalletTransaction(selectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
PSBT psbt = walletTransaction.createPSBT();
}
private long getRecipientDustThreshold(Address address) {
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
return address.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
}
}

View file

@ -1,24 +1,118 @@
package com.sparrowwallet.sparrow.joinstr;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.BnBUtxoSelector;
import com.sparrowwallet.drongo.wallet.CoinbaseTxoFilter;
import com.sparrowwallet.drongo.wallet.FrozenTxoFilter;
import com.sparrowwallet.drongo.wallet.InsufficientFundsException;
import com.sparrowwallet.drongo.wallet.KnapsackUtxoSelector;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.SpentTxoFilter;
import com.sparrowwallet.drongo.wallet.TxoFilter;
import com.sparrowwallet.drongo.wallet.UtxoSelector;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.control.Label;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
public class NewPoolController extends JoinstrFormController {
@FXML
private Label addressLabel;
@FXML
private TextField labelField;
@FXML
private TextField amountField;
@FXML
private TextField denominationField;
@FXML
private TextField peersField;
@FXML
private ComboBox<BlockTransactionHashIndex> utxosComboBox;
@FXML
private ComboBox<BitcoinUnit> denominationUnit;
private Address coinjoinAddress;
@Override
public void initializeView() {
coinjoinAddress = getNewReceiveAddress();
addressLabel.setText(coinjoinAddress.getAddress());
ObservableList<BlockTransactionHashIndex> utxos = FXCollections.observableArrayList(getWalletForm().getWallet().getWalletTxos().keySet().stream().filter(ref -> !ref.isSpent()).collect(Collectors.toList()));
if(!utxos.isEmpty()) {
utxosComboBox.setItems(utxos);
}
utxosComboBox.setPromptText("Select an UTXO");
denominationUnit.setValue(BitcoinUnit.SATOSHIS);
}
@FXML
private void handleCreateButton() {
private void setUtxoAmount(ActionEvent event) {
switch(denominationUnit.getValue()) {
case BTC -> {
amountField.setText((utxosComboBox.getValue().getValue()) + " BTC");
}
case SATOSHIS -> {
amountField.setText((utxosComboBox.getValue().getValue()) + " sats");
}
}
}
@FXML
private void handleDenominationUnitChange(ActionEvent event) {
switch(denominationUnit.getValue()) {
case BTC -> {
if(!amountField.getText().isEmpty()) {
amountField.setText((Double.parseDouble(amountField.getText().split(" ")[0]) / 100000000) + " BTC");
}
}
case SATOSHIS -> {
double amount = Double.parseDouble(amountField.getText().split(" ")[0]) * 100000000;
amountField.setText(String.valueOf(amount).split("\\.")[0] + " sats");
}
}
}
@FXML
private void handleCreateButton(ActionEvent event) {
try {
String denomination = denominationField.getText().trim();
@ -31,6 +125,9 @@ public class NewPoolController extends JoinstrFormController {
try {
double denominationValue = Double.parseDouble(denomination);
if(denominationUnit.getValue() == BitcoinUnit.SATOSHIS) {
Long.parseLong(denomination);
}
if (denominationValue <= 0) {
showError("Denomination must be greater than 0");
return;
@ -51,11 +148,50 @@ public class NewPoolController extends JoinstrFormController {
return;
}
long denominationSats = Long.valueOf((long) (Double.parseDouble(denomination)*100000000.0D));
String amount = amountField.getText().trim();
if (amount.isEmpty()) {
showError("Please select an UTXO to create a pool.");
return;
}
amount = amount.split(" ")[0]; // Removes BTC/sats
try {
double amountValue = Double.parseDouble(amount);
if(denominationUnit.getValue() == BitcoinUnit.SATOSHIS) {
Long.parseLong(amount);
}
if (amountValue != Double.parseDouble(denomination) && amountValue < Double.parseDouble(denomination) + getRecipientDustThreshold(coinjoinAddress)) {
showError("Amount is smaller than denomination with dust threshold");
return;
}
} catch (NumberFormatException e) {
showError("Invalid amount format");
return;
}
long amountInSats;
long denominationInSats;
if(denominationUnit.getValue() == BitcoinUnit.SATOSHIS) {
denominationInSats = Long.parseLong(denomination);
amountInSats = Long.parseLong(amount);
} else {
denominationInSats = (long)Double.parseDouble(denomination) * 100000000;
amountInSats = (long)Double.parseDouble(amount) * 100000000;
}
JoinstrPool pool = new JoinstrPool(9999, "pubkey", denominationInSats);
// TODO: Publish a nostr event with pool info and wait for peers
pool.publicNostrEvent();
pool.waitForPeers();
// TODO: Implement pool creation logic here
JoinstrPool pool = new JoinstrPool(getWalletForm(), Config.get().getNostrRelay(),9999, "pubkey", denominationSats);
pool.createPSBT();
PSBT myPoolPSBT = createPSBT(labelField.getText(), amountInSats, denominationInSats);
if(myPoolPSBT != null) {
/// TODO: Sign using sighash flag ALL | ACP
/// TODO: Combine all PSBTs
}
/*
Alert alert = new Alert(AlertType.INFORMATION);
@ -67,12 +203,54 @@ public class NewPoolController extends JoinstrFormController {
denominationField.clear();
peersField.clear();
amountField.clear();
labelField.clear();
} catch (Exception e) {
showError("An error occurred: " + e.getMessage());
}
}
private PSBT createPSBT(String paymentLabel, long amount, long denomination) throws InsufficientFundsException {
// PSBT from WalletTransaction
double feeRate = 1.0;
double longTermFeeRate = 10.0;
long fee = 10L;
long dustThreshold = getRecipientDustThreshold(coinjoinAddress);
long satsLeft = amount;
ArrayList<Payment> payments = new ArrayList<Payment>();
while(satsLeft == denomination || satsLeft > denomination + dustThreshold) {
Payment payment = new Payment(coinjoinAddress, paymentLabel, denomination, false);
satsLeft -= denomination;
payment.setType(Payment.Type.COINJOIN);
payments.add(payment);
}
long noInputsFee = getWalletForm().getWallet().getNoInputsFee(payments, feeRate);
long costOfChange = getWalletForm().getWallet().getCostOfChange(feeRate, longTermFeeRate);
List<UtxoSelector> selectors = new ArrayList<>(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee)));
SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null);
List<TxoFilter> txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet()));
ArrayList<byte[]> opReturns = new ArrayList<>();
TreeSet<WalletNode> excludedChangeNodes = new TreeSet<>();
Integer currentBlockHeight = AppServices.getCurrentBlockHeight();
boolean groupByAddress = false;
boolean includeMempoolOutputs = false;
WalletTransaction walletTransaction = getWalletForm().getWallet().createWalletTransaction(selectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs);
return walletTransaction.createPSBT();
}
private long getRecipientDustThreshold(Address address) {
TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript());
return address.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
}
private void showError(String message) {
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
@ -80,4 +258,10 @@ public class NewPoolController extends JoinstrFormController {
alert.setContentText(message);
alert.showAndWait();
}
private Address getNewReceiveAddress() {
NodeEntry freshNodeEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, null);
return freshNodeEntry.getAddress();
}
}

View file

@ -24,16 +24,21 @@ public class SettingsController extends JoinstrFormController {
@Override
public void changed(ObservableValue<? extends String> observable,
String oldValue, String newValue) {
if(nostrRelayTextField.getText().isEmpty()) {
nostrRelayTextField.setText("wss://nostr.fmt.wiz.biz");
}
setDefaultNostrRelayIfEmpty();
Config.get().setNostrRelay(nostrRelayTextField.getText());
}
});
setDefaultNostrRelayIfEmpty();
} catch(Exception e) {
e.printStackTrace();
}
}
public void setDefaultNostrRelayIfEmpty() {
if(nostrRelayTextField.getText().isEmpty()) {
nostrRelayTextField.setText("wss://nostr.fmt.wiz.biz");
}
}
}

View file

@ -3,36 +3,62 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.collections.FXCollections?>
<BorderPane stylesheets="@joinstr.css, @../wallet/wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.joinstr.NewPoolController">
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<BorderPane stylesheets="@joinstr.css, @../wallet/wallet.css, @../general.css" styleClass="send-form" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.joinstr.NewPoolController">
<padding>
<Insets top="30" right="30" bottom="30" left="30"/>
</padding>
<center>
<VBox maxWidth="Infinity" fx:id="contentVBox" spacing="20">
<VBox maxWidth="Infinity" HBox.hgrow="ALWAYS">
<Label styleClass="sub-title">Create a new pool</Label>
</VBox>
<GridPane hgap="10" vgap="15" maxWidth="600">
<padding>
<Insets top="20"/>
</padding>
<Label text="Denomination (BTC):" GridPane.rowIndex="0" GridPane.columnIndex="0" style="-fx-text-fill: #aaaaaa;"/>
<TextField fx:id="denominationField" GridPane.rowIndex="0" GridPane.columnIndex="1"
style="-fx-background-color: #444444; -fx-text-fill: white; -fx-prompt-text-fill: #888888;"
promptText="Enter denomination"/>
<Label text="Number of Peers:" GridPane.rowIndex="1" GridPane.columnIndex="0" style="-fx-text-fill: #aaaaaa;"/>
<TextField fx:id="peersField" GridPane.rowIndex="1" GridPane.columnIndex="1"
style="-fx-background-color: #444444; -fx-text-fill: white; -fx-prompt-text-fill: #888888;"
promptText="Enter number of peers"/>
<Button fx:id="createButton" text="Create" GridPane.rowIndex="2" GridPane.columnIndex="1"
onAction="#handleCreateButton"
style="-fx-background-color: #2196F3; -fx-text-fill: white; -fx-cursor: hand;"/>
</GridPane>
<Form maxWidth="Infinity" style="-fx-max-width: 600px;">
<Fieldset inputGrow="ALWAYS">
<Field text="Pay to:">
<Label fx:id="addressLabel" />
</Field>
<Field text="Label:">
<TextField fx:id="labelField" promptText="Required" style="-fx-max-width: 250px;">
<tooltip>
<Tooltip text="Required to label the transaction (privately in this wallet)"/>
</tooltip>
</TextField>
</Field>
<Field text="UTXO:">
<ComboBox fx:id="utxosComboBox" styleClass="amount-unit" style="-fx-max-width: 150px;" onAction="#setUtxoAmount" />
<TextField fx:id="amountField" disable="true" style="-fx-max-width: 100px;-fx-text-fill: white;" />
</Field>
<Field text="Denomination:" style="-fx-text-fill: #aaaaaa;">
<TextField fx:id="denominationField" promptText="Required" style="-fx-max-width: 100px;-fx-background-color: #444444; -fx-text-fill: white; -fx-prompt-text-fill: #888888;">
<tooltip>
<Tooltip text="Required to create coinjoin pool"/>
</tooltip>
</TextField>
<ComboBox fx:id="denominationUnit" styleClass="amount-unit" style="-fx-min-width: 60px;" onAction="#handleDenominationUnitChange">
<items>
<FXCollections fx:factory="observableArrayList">
<BitcoinUnit fx:constant="BTC" />
<BitcoinUnit fx:constant="SATOSHIS" />
</FXCollections>
</items>
</ComboBox>
</Field>
<Field text="Number of Peers:" style="-fx-text-fill: #aaaaaa;">
<TextField fx:id="peersField" promptText="Req." style="-fx-max-width: 50px;-fx-background-color: #444444; -fx-text-fill: white; -fx-prompt-text-fill: #888888;" />
</Field>
<Field>
<ToggleButton fx:id="createButton" text="Create" onAction="#handleCreateButton" style="-fx-background-color: #2196F3; -fx-text-fill: white; -fx-cursor: hand;" />
</Field>
</Fieldset>
</Form>
</VBox>
</center>
</BorderPane>