diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java index cda16b7f..2385be7d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java @@ -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() + private final long denomination; // Should be in sats, like transaction amounts - 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); - } - - */ - - /* PSBT from QR Code - - QRScanDialog qrScanDialog = new QRScanDialog(); - qrScanDialog.initOwner(rootStack.getScene().getWindow()); - Optional 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 payments = new ArrayList(); - while(satsLeft > denomination + dustThreshold) { - Payment payment = new Payment(sendAddress, paymentLabel, denomination, false); - satsLeft -= denomination; - payment.setType(Payment.Type.COINJOIN); - payments.add(payment); - } - - List 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 txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(walletForm.getWallet())); - - ArrayList opReturns = new ArrayList<>(); - TreeSet 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(); - - + public void waitForPeers() { + // TODO: Wait for others to join } - private long getRecipientDustThreshold(Address address) { - TransactionOutput txOutput = new TransactionOutput(new Transaction(), 1L, address.getOutputScript()); - return address.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE); - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java index 71a6532a..1631e360 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java @@ -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 utxosComboBox; + + @FXML + private ComboBox denominationUnit; + + private Address coinjoinAddress; + @Override public void initializeView() { + + coinjoinAddress = getNewReceiveAddress(); + addressLabel.setText(coinjoinAddress.getAddress()); + + ObservableList 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 payments = new ArrayList(); + 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 selectors = new ArrayList<>(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee))); + + SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null); + List txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet())); + + ArrayList opReturns = new ArrayList<>(); + TreeSet 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(); + } + } diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/SettingsController.java index 45e5ada1..89eb6741 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/SettingsController.java @@ -24,16 +24,21 @@ public class SettingsController extends JoinstrFormController { @Override public void changed(ObservableValue 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"); + } + } + } diff --git a/src/main/resources/com/sparrowwallet/sparrow/joinstr/new_pool.fxml b/src/main/resources/com/sparrowwallet/sparrow/joinstr/new_pool.fxml index d6868a09..36972b09 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/joinstr/new_pool.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/joinstr/new_pool.fxml @@ -3,36 +3,62 @@ + - + + + + + + + + +
- +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- - - - - - -
\ No newline at end of file