From b1a3dc6411bb3d30a82de8729c4ff4a27fea3e60 Mon Sep 17 00:00:00 2001 From: QcMrHyde Date: Thu, 29 May 2025 01:29:20 -0400 Subject: [PATCH 1/4] - Added code findings about PSBT & SigHash --- .../sparrow/joinstr/JoinstrPool.java | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java index ca1515d3..40eb6ddd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java @@ -1,18 +1,30 @@ package com.sparrowwallet.sparrow.joinstr; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.sparrow.control.QRScanDialog; +import com.sparrowwallet.sparrow.net.Tor; +import com.sparrowwallet.sparrow.wallet.NodeEntry; +import com.sparrowwallet.sparrow.wallet.WalletForm; + +import java.util.Optional; + public class JoinstrPool { private final String relay; private final Integer port; private final String pubkey; private final Double denomination; + private final WalletForm walletForm; // [FormController].getWalletForm() - public JoinstrPool(String relay_, Integer port_, String pubkey_, Double denomination_) { + public JoinstrPool(WalletForm walletForm,String relay, Integer port, String pubkey, Double denomination) { - relay = relay_; - port = port_; - pubkey = pubkey_; - denomination = denomination_; + this.walletForm = walletForm; + this.relay = relay; + this.port = port; + this.pubkey = pubkey; + this.denomination = denomination; } @@ -32,4 +44,79 @@ public class JoinstrPool { return denomination; } + public void getNewTorRoute() { + + Tor tor = Tor.getDefault(); + tor.changeIdentity(); + + } + + private Address getNewSendAddress() { + NodeEntry freshNodeEntry = walletForm.getFreshNodeEntry(KeyPurpose.SEND, null); + return freshNodeEntry.getAddress(); + } + + public void createPSBT() { + + Address sendAddress = getNewSendAddress(); + PSBT psbt; + + /* String to byte[] + + if(Utils.isBase64(string) && !Utils.isHex(string)) { + addTransactionTab(name, file, Base64.getDecoder().decode(string)); + } else if(Utils.isHex(string)) { + addTransactionTab(name, file, Utils.hexToBytes(string)); + } else { + throw new ParseException("Input is not base64 or hex", 0); + } + + */ + + /* 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 TransactionData + TransactionData txdata; + /// TODO fill txdata + psbt = txdata.getPsbt(); + */ + + /* PSBT from Transaction + + Transaction toSign; + /// TODO fill toSign + + PSBT psbt = new PSBT(toSign); + PSBTInput psbtInput = psbt.getPsbtInputs().get(0); + + Transaction toSpend; + /// TODO fill toSpend + TransactionOutput utxoOutput = toSpend.getOutputs().get(0); + psbtInput.setWitnessUtxo(utxoOutput); + + psbtInput.setSigHash(SigHash.ALL); + psbtInput.sign(scriptType.getOutputKey(privKey)); + + */ + + } + } From 055bc65bff558cc109a5bf159ec054ab3a06abd7 Mon Sep 17 00:00:00 2001 From: QcMrHyde Date: Fri, 30 May 2025 16:45:43 -0400 Subject: [PATCH 2/4] - Added code findings for WalletTransaction creation --- .../sparrow/joinstr/JoinstrPool.java | 54 ++++++++++++++++--- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java index 40eb6ddd..29bc6257 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java @@ -2,23 +2,33 @@ 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.psbt.PSBT; +import com.sparrowwallet.drongo.wallet.CoinbaseTxoFilter; +import com.sparrowwallet.drongo.wallet.FrozenTxoFilter; +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.control.QRScanDialog; import com.sparrowwallet.sparrow.net.Tor; import com.sparrowwallet.sparrow.wallet.NodeEntry; import com.sparrowwallet.sparrow.wallet.WalletForm; -import java.util.Optional; +import java.util.*; public class JoinstrPool { private final String relay; private final Integer port; private final String pubkey; - private final Double denomination; + 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, Double denomination) { + public JoinstrPool(WalletForm walletForm,String relay, Integer port, String pubkey, long denomination) { this.walletForm = walletForm; this.relay = relay; @@ -40,7 +50,7 @@ public class JoinstrPool { return pubkey; } - public Double getDenomination() { + public long getDenomination() { return denomination; } @@ -59,7 +69,6 @@ public class JoinstrPool { public void createPSBT() { Address sendAddress = getNewSendAddress(); - PSBT psbt; /* String to byte[] @@ -99,10 +108,41 @@ public class JoinstrPool { psbt = txdata.getPsbt(); */ + // PSBT from WalletTransaction + + List utxoSelectors; + /// TODO select UTXO for transaction + + SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null); + List txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(walletForm.getWallet())); + + long satsLeft = 0L; + Payment coinjoinPayment = new Payment(sendAddress, "coinjoin", denomination, false); + /// TODO create multiple coinjoin payments for selected UTXO + Payment changePayment = new Payment(sendAddress, "change", satsLeft, false); + List payments = List.of(coinjoinPayment, changePayment); + + List opReturns = null; + + Set excludedChangeNodes; + /// TODO fill excludedChangeNodes + + double feeRate = 10.0; + double longTermFeeRate = 10.0; + Long fee = 10L; + Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); + boolean groupByAddress = false; + boolean includeMempoolOutputs = false; + + WalletTransaction toSign = walletForm.getWallet().createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); + PSBT psbt = toSign.createPSBT(); + + // decryptedWallet.sign(psbt); + // decryptedWallet.finalise(psbt); + // Transaction transaction = psbt.extractTransaction(); + /* PSBT from Transaction - Transaction toSign; - /// TODO fill toSign PSBT psbt = new PSBT(toSign); PSBTInput psbtInput = psbt.getPsbtInputs().get(0); From 550df1d91a9944321164434e9cd43b4cbcda78d7 Mon Sep 17 00:00:00 2001 From: QcMrHyde Date: Mon, 2 Jun 2025 04:12:25 -0400 Subject: [PATCH 3/4] - Added code to create coinjoin PSBT --- .../sparrowwallet/sparrow/AppController.java | 2 +- .../sparrow/joinstr/JoinstrController.java | 35 +++--- .../sparrow/joinstr/JoinstrForm.java | 17 +-- .../joinstr/JoinstrFormController.java | 7 +- .../sparrow/joinstr/JoinstrPool.java | 106 ++++++++---------- .../sparrow/joinstr/NewPoolController.java | 6 + 6 files changed, 85 insertions(+), 88 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 32d142b0..df2286ab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -556,7 +556,7 @@ public class AppController implements Initializable { stage.initOwner(tabs.getScene().getWindow()); JoinstrController controller = loader.getController(); - JoinstrForm joinstrForm = new JoinstrForm(getSelectedWalletForm().getStorage(), getSelectedWalletForm().getWallet()); + JoinstrForm joinstrForm = new JoinstrForm(getSelectedWalletForm()); controller.setJoinstrForm(joinstrForm); Scene scene = new Scene(root); diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrController.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrController.java index 35a60a25..522ba5c5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrController.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrController.java @@ -57,26 +57,21 @@ public class JoinstrController extends JoinstrFormController { } try { - URL url = AppServices.class.getResource("joinstr/" + display.toString().toLowerCase(Locale.ROOT) + ".fxml"); - if(url == null) { - throw new IllegalStateException("Cannot find joinstr/" + display.toString().toLowerCase(Locale.ROOT) + ".fxml"); - } - - FXMLLoader displayLoader = new FXMLLoader(url); - Node joinstrDisplay = displayLoader.load(); - - if(!existing) { - - joinstrDisplay.setUserData(display); - joinstrDisplay.setViewOrder(1); - - joinstrPane.getChildren().add(joinstrDisplay); - } - - JoinstrFormController controller = displayLoader.getController(); - JoinstrForm joinstrForm = getJoinstrForm(); - controller.setJoinstrForm(joinstrForm); - controller.initializeView(); + URL url = AppServices.class.getResource("joinstr/" + display.toString().toLowerCase(Locale.ROOT) + ".fxml"); + if(url == null) { + throw new IllegalStateException("Cannot find joinstr/" + display.toString().toLowerCase(Locale.ROOT) + ".fxml"); + } + FXMLLoader displayLoader = new FXMLLoader(url); + Node joinstrDisplay = displayLoader.load(); + if(!existing) { + joinstrDisplay.setUserData(display); + joinstrDisplay.setViewOrder(1); + joinstrPane.getChildren().add(joinstrDisplay); + } + JoinstrFormController controller = displayLoader.getController(); + JoinstrForm joinstrForm = getJoinstrForm(); + controller.setJoinstrForm(joinstrForm); + controller.initializeView(); } catch (IOException e) { throw new IllegalStateException("Can't find pane", e); diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrForm.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrForm.java index 03807dfd..019ec6f1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrForm.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.joinstr; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.wallet.WalletForm; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -10,20 +11,22 @@ public class JoinstrForm { private final BooleanProperty lockedProperty = new SimpleBooleanProperty(false); - private final Storage storage; - protected Wallet wallet; + private final WalletForm walletForm; - public JoinstrForm(Storage storage, Wallet currentWallet) { - this.storage = storage; - this.wallet = currentWallet; + public JoinstrForm(WalletForm walletForm) { + this.walletForm = walletForm; + } + + public WalletForm getWalletForm() { + return walletForm; } public Wallet getWallet() { - return wallet; + return walletForm.getWallet(); } public Storage getStorage() { - return storage; + return walletForm.getStorage(); } public BooleanProperty lockedProperty() { diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrFormController.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrFormController.java index 9e631e55..bdd3c3a7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrFormController.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrFormController.java @@ -1,10 +1,15 @@ package com.sparrowwallet.sparrow.joinstr; import com.sparrowwallet.sparrow.BaseController; +import com.sparrowwallet.sparrow.wallet.WalletForm; public abstract class JoinstrFormController extends BaseController { - public JoinstrForm joinstrForm; + private JoinstrForm joinstrForm; + + public WalletForm getWalletForm() { + return joinstrForm.getWalletForm(); + } public JoinstrForm getJoinstrForm() { return joinstrForm; diff --git a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java index 29bc6257..cda16b7f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/JoinstrPool.java @@ -3,20 +3,32 @@ 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; 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.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.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.*; @@ -66,21 +78,7 @@ public class JoinstrPool { return freshNodeEntry.getAddress(); } - public void createPSBT() { - - Address sendAddress = getNewSendAddress(); - - /* String to byte[] - - if(Utils.isBase64(string) && !Utils.isHex(string)) { - addTransactionTab(name, file, Base64.getDecoder().decode(string)); - } else if(Utils.isHex(string)) { - addTransactionTab(name, file, Utils.hexToBytes(string)); - } else { - throw new ParseException("Input is not base64 or hex", 0); - } - - */ + public void createPSBT() throws InsufficientFundsException { /* PSBT from byte[] @@ -102,61 +100,51 @@ public class JoinstrPool { result.psbt; */ - /* PSBT from TransactionData - TransactionData txdata; - /// TODO fill txdata - psbt = txdata.getPsbt(); - */ - // PSBT from WalletTransaction - List utxoSelectors; - /// TODO select UTXO for transaction + 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; - SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null); - List txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(walletForm.getWallet())); + String paymentLabel = "coinjoin"; - long satsLeft = 0L; - Payment coinjoinPayment = new Payment(sendAddress, "coinjoin", denomination, false); - /// TODO create multiple coinjoin payments for selected UTXO - Payment changePayment = new Payment(sendAddress, "change", satsLeft, false); - List payments = List.of(coinjoinPayment, changePayment); + 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 opReturns = null; + 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))); - Set excludedChangeNodes; - /// TODO fill excludedChangeNodes + SpentTxoFilter spentTxoFilter = new SpentTxoFilter(null); + List txoFilters = List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(walletForm.getWallet())); - double feeRate = 10.0; - double longTermFeeRate = 10.0; - Long fee = 10L; - Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); - boolean groupByAddress = false; - boolean includeMempoolOutputs = false; + ArrayList opReturns = new ArrayList<>(); + TreeSet excludedChangeNodes = new TreeSet<>(); - WalletTransaction toSign = walletForm.getWallet().createWalletTransaction(utxoSelectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); - PSBT psbt = toSign.createPSBT(); + Integer currentBlockHeight = AppServices.getCurrentBlockHeight(); + boolean groupByAddress = false; + boolean includeMempoolOutputs = false; - // decryptedWallet.sign(psbt); - // decryptedWallet.finalise(psbt); - // Transaction transaction = psbt.extractTransaction(); + WalletTransaction walletTransaction = walletForm.getWallet().createWalletTransaction(selectors, txoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs); + PSBT psbt = walletTransaction.createPSBT(); - /* PSBT from Transaction - - - PSBT psbt = new PSBT(toSign); - PSBTInput psbtInput = psbt.getPsbtInputs().get(0); - - Transaction toSpend; - /// TODO fill toSpend - TransactionOutput utxoOutput = toSpend.getOutputs().get(0); - psbtInput.setWitnessUtxo(utxoOutput); - - psbtInput.setSigHash(SigHash.ALL); - psbtInput.sign(scriptType.getOutputKey(privKey)); - - */ } + 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 e6e6413b..71a6532a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java +++ b/src/main/java/com/sparrowwallet/sparrow/joinstr/NewPoolController.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.joinstr; +import com.sparrowwallet.sparrow.io.Config; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.control.Alert; @@ -19,6 +20,7 @@ public class NewPoolController extends JoinstrFormController { @FXML private void handleCreateButton() { try { + String denomination = denominationField.getText().trim(); String peers = peersField.getText().trim(); @@ -49,7 +51,11 @@ public class NewPoolController extends JoinstrFormController { return; } + long denominationSats = Long.valueOf((long) (Double.parseDouble(denomination)*100000000.0D)); + // TODO: Implement pool creation logic here + JoinstrPool pool = new JoinstrPool(getWalletForm(), Config.get().getNostrRelay(),9999, "pubkey", denominationSats); + pool.createPSBT(); /* Alert alert = new Alert(AlertType.INFORMATION); From 435f12693334b8ab45e1172bc0d9d93eab7bc913 Mon Sep 17 00:00:00 2001 From: QcMrHyde Date: Tue, 3 Jun 2025 00:18:09 -0400 Subject: [PATCH 4/4] Code cleanup & refactoring --- .../sparrow/joinstr/JoinstrPool.java | 98 +-------- .../sparrow/joinstr/NewPoolController.java | 192 +++++++++++++++++- .../sparrow/joinstr/SettingsController.java | 11 +- .../sparrow/joinstr/new_pool.fxml | 72 ++++--- 4 files changed, 255 insertions(+), 118 deletions(-) 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