mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add tool to sweep a private key in wif format to any address
This commit is contained in:
parent
7f2d72ee59
commit
9c3b647f07
12 changed files with 461 additions and 15 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 3a557e3af8bd17abf7697f93e586baf67745b460
|
Subproject commit 34bd72d87aac7286fd0ca7e94f5a931f00d13cb4
|
|
@ -161,6 +161,9 @@ public class AppController implements Initializable {
|
||||||
@FXML
|
@FXML
|
||||||
private MenuItem sendToMany;
|
private MenuItem sendToMany;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private MenuItem sweepPrivateKey;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private MenuItem findMixingPartner;
|
private MenuItem findMixingPartner;
|
||||||
|
|
||||||
|
@ -333,6 +336,7 @@ public class AppController implements Initializable {
|
||||||
lockWallet.setDisable(true);
|
lockWallet.setDisable(true);
|
||||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||||
|
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||||
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
|
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
|
||||||
findMixingPartner.setDisable(true);
|
findMixingPartner.setDisable(true);
|
||||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -1230,6 +1234,18 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sweepPrivateKey(ActionEvent event) {
|
||||||
|
Wallet wallet = null;
|
||||||
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
|
if(selectedWalletForm != null) {
|
||||||
|
wallet = selectedWalletForm.getWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivateKeySweepDialog dialog = new PrivateKeySweepDialog(wallet);
|
||||||
|
Optional<Transaction> optTransaction = dialog.showAndWait();
|
||||||
|
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||||
|
}
|
||||||
|
|
||||||
public void findMixingPartner(ActionEvent event) {
|
public void findMixingPartner(ActionEvent event) {
|
||||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||||
if(selectedWalletForm != null) {
|
if(selectedWalletForm != null) {
|
||||||
|
|
|
@ -76,6 +76,10 @@ public class AppServices {
|
||||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||||
|
|
||||||
|
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||||
|
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||||
|
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||||
|
|
||||||
private static AppServices INSTANCE;
|
private static AppServices INSTANCE;
|
||||||
|
|
||||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||||
|
@ -591,6 +595,11 @@ public class AppServices {
|
||||||
return latestBlockHeader;
|
return latestBlockHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Double getDefaultFeeRate() {
|
||||||
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
|
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||||
return targetBlockFeeRates;
|
return targetBlockFeeRates;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.chart.Axis;
|
import javafx.scene.chart.Axis;
|
||||||
|
@ -28,7 +28,7 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||||
|
|
||||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||||
Integer targetBlocks = targetBlocksIter.next();
|
Integer targetBlocks = targetBlocksIter.next();
|
||||||
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||||
feeRateSeries.getData().add(data);
|
feeRateSeries.getData().add(data);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
@ -79,7 +79,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
});
|
});
|
||||||
|
|
||||||
long previousFeeRate = 0;
|
long previousFeeRate = 0;
|
||||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
||||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||||
series.setName(feeRate + "+ sats/vB");
|
series.setName(feeRate + "+ sats/vB");
|
||||||
long seriesTotalVSize = 0;
|
long seriesTotalVSize = 0;
|
||||||
|
|
|
@ -103,6 +103,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
|
|
||||||
final DialogPane dialogPane = getDialogPane();
|
final DialogPane dialogPane = getDialogPane();
|
||||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||||
|
|
||||||
|
@ -120,6 +121,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
Form form = new Form();
|
Form form = new Form();
|
||||||
Fieldset fieldset = new Fieldset();
|
Fieldset fieldset = new Fieldset();
|
||||||
fieldset.setText("");
|
fieldset.setText("");
|
||||||
|
fieldset.setSpacing(10);
|
||||||
|
|
||||||
Field addressField = new Field();
|
Field addressField = new Field();
|
||||||
addressField.setText("Address:");
|
addressField.setText("Address:");
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.io.Files;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import tornadofx.control.Field;
|
||||||
|
import tornadofx.control.Fieldset;
|
||||||
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||||
|
|
||||||
|
public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PrivateKeySweepDialog.class);
|
||||||
|
|
||||||
|
private final TextArea key;
|
||||||
|
private final ComboBox<ScriptType> keyScriptType;
|
||||||
|
private final CopyableLabel keyAddress;
|
||||||
|
private final ComboBoxTextField toAddress;
|
||||||
|
private final ComboBox<Wallet> toWallet;
|
||||||
|
|
||||||
|
public PrivateKeySweepDialog(Wallet wallet) {
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
dialogPane.setHeaderText("Sweep Private Key");
|
||||||
|
|
||||||
|
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||||
|
if(!image.isError()) {
|
||||||
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setSmooth(false);
|
||||||
|
imageView.setImage(image);
|
||||||
|
dialogPane.setGraphic(imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
VBox vBox = new VBox();
|
||||||
|
vBox.setSpacing(20);
|
||||||
|
|
||||||
|
Form form = new Form();
|
||||||
|
Fieldset fieldset = new Fieldset();
|
||||||
|
fieldset.setText("");
|
||||||
|
fieldset.setSpacing(10);
|
||||||
|
|
||||||
|
Field keyField = new Field();
|
||||||
|
keyField.setText("Private Key:");
|
||||||
|
key = new TextArea();
|
||||||
|
key.setWrapText(true);
|
||||||
|
key.setPromptText("Wallet Import Format (WIF)");
|
||||||
|
key.setPrefRowCount(2);
|
||||||
|
key.getStyleClass().add("fixed-width");
|
||||||
|
HBox keyBox = new HBox(5);
|
||||||
|
VBox keyButtonBox = new VBox(5);
|
||||||
|
Button scanKey = new Button("", getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||||
|
scanKey.setOnAction(event -> scanPrivateKey());
|
||||||
|
Button readKey = new Button("", getGlyph(FontAwesome5.Glyph.FILE_IMPORT));
|
||||||
|
readKey.setOnAction(event -> readPrivateKey());
|
||||||
|
keyButtonBox.getChildren().addAll(scanKey, readKey);
|
||||||
|
keyBox.getChildren().addAll(key, keyButtonBox);
|
||||||
|
HBox.setHgrow(key, Priority.ALWAYS);
|
||||||
|
keyField.getInputs().add(keyBox);
|
||||||
|
|
||||||
|
Field keyScriptTypeField = new Field();
|
||||||
|
keyScriptTypeField.setText("Script Type:");
|
||||||
|
keyScriptType = new ComboBox<>();
|
||||||
|
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||||
|
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||||
|
|
||||||
|
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||||
|
@Override
|
||||||
|
public String toString(ScriptType scriptType) {
|
||||||
|
return scriptType == null ? "" : scriptType.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptType fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Field addressField = new Field();
|
||||||
|
addressField.setText("Address:");
|
||||||
|
keyAddress = new CopyableLabel();
|
||||||
|
keyAddress.getStyleClass().add("fixed-width");
|
||||||
|
addressField.getInputs().add(keyAddress);
|
||||||
|
|
||||||
|
Field toAddressField = new Field();
|
||||||
|
toAddressField.setText("Sweep to:");
|
||||||
|
toAddress = new ComboBoxTextField();
|
||||||
|
toAddress.getStyleClass().add("fixed-width");
|
||||||
|
toWallet = new ComboBox<>();
|
||||||
|
toWallet.setItems(FXCollections.observableList(new ArrayList<>(AppServices.get().getOpenWallets().keySet())));
|
||||||
|
toAddress.setComboProperty(toWallet);
|
||||||
|
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().addAll(toWallet, toAddress);
|
||||||
|
toAddressField.getInputs().add(stackPane);
|
||||||
|
|
||||||
|
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField);
|
||||||
|
form.getChildren().add(fieldset);
|
||||||
|
dialogPane.setContent(form);
|
||||||
|
|
||||||
|
ButtonType createButtonType = new javafx.scene.control.ButtonType("Create Transaction", ButtonBar.ButtonData.APPLY);
|
||||||
|
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
|
||||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType, createButtonType);
|
||||||
|
|
||||||
|
Button createButton = (Button) dialogPane.lookupButton(createButtonType);
|
||||||
|
createButton.setDefaultButton(true);
|
||||||
|
createButton.setDisable(true);
|
||||||
|
createButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||||
|
createTransaction();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
|
key.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
boolean isValidKey = isValidKey();
|
||||||
|
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||||
|
if(isValidKey) {
|
||||||
|
setFromAddress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
keyScriptType.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(isValidKey()) {
|
||||||
|
setFromAddress();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||||
|
});
|
||||||
|
|
||||||
|
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||||
|
if(selectedWallet != null) {
|
||||||
|
toAddress.setText(selectedWallet.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
keyScriptType.setValue(ScriptType.P2PKH);
|
||||||
|
if(wallet != null) {
|
||||||
|
toAddress.setText(wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
setResultConverter(dialogButton -> null);
|
||||||
|
dialogPane.setPrefWidth(680);
|
||||||
|
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
validationSupport.registerValidator(key, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid private Key", !key.getText().isEmpty() && !isValidKey()));
|
||||||
|
validationSupport.registerValidator(toAddress, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !toAddress.getText().isEmpty() && !isValidToAddress()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidKey() {
|
||||||
|
try {
|
||||||
|
DumpedPrivateKey privateKey = getPrivateKey();
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DumpedPrivateKey getPrivateKey() {
|
||||||
|
return DumpedPrivateKey.fromBase58(key.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidToAddress() {
|
||||||
|
try {
|
||||||
|
Address address = getToAddress();
|
||||||
|
return true;
|
||||||
|
} catch (InvalidAddressException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getToAddress() throws InvalidAddressException {
|
||||||
|
return Address.fromString(toAddress.getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFromAddress() {
|
||||||
|
DumpedPrivateKey privateKey = getPrivateKey();
|
||||||
|
ScriptType scriptType = keyScriptType.getValue();
|
||||||
|
Address address = scriptType.getAddress(privateKey.getKey());
|
||||||
|
keyAddress.setText(address.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scanPrivateKey() {
|
||||||
|
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||||
|
Optional<QRScanDialog.Result> result = qrScanDialog.showAndWait();
|
||||||
|
if(result.isPresent() && result.get().payload != null) {
|
||||||
|
key.setText(result.get().payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readPrivateKey() {
|
||||||
|
Stage window = new Stage();
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Open Private Key File");
|
||||||
|
|
||||||
|
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||||
|
File file = fileChooser.showOpenDialog(window);
|
||||||
|
if(file != null) {
|
||||||
|
if(file.length() > 1024) {
|
||||||
|
AppServices.showErrorDialog("Invalid private key file", "This file does not contain a valid private key.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
key.setText(Files.asCharSource(file, StandardCharsets.UTF_8).read().trim());
|
||||||
|
} catch(IOException e) {
|
||||||
|
AppServices.showErrorDialog("Error reading private key file", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTransaction() {
|
||||||
|
try {
|
||||||
|
DumpedPrivateKey privateKey = getPrivateKey();
|
||||||
|
ScriptType scriptType = keyScriptType.getValue();
|
||||||
|
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||||
|
Address destAddress = getToAddress();
|
||||||
|
|
||||||
|
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||||
|
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||||
|
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||||
|
});
|
||||||
|
addressUtxosService.setOnFailed(failedEvent -> {
|
||||||
|
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||||
|
});
|
||||||
|
addressUtxosService.start();
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error creating sweep transaction", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||||
|
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||||
|
|
||||||
|
Transaction noFeeTransaction = new Transaction();
|
||||||
|
long total = 0;
|
||||||
|
for(TransactionOutput txOutput : txOutputs) {
|
||||||
|
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||||
|
total += txOutput.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
|
||||||
|
noFeeTransaction.addOutput(sweepOutput);
|
||||||
|
|
||||||
|
Double feeRate = AppServices.getDefaultFeeRate();
|
||||||
|
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||||
|
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
|
fee++;
|
||||||
|
}
|
||||||
|
|
||||||
|
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||||
|
if(total - fee <= dustThreshold) {
|
||||||
|
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction transaction = new Transaction();
|
||||||
|
transaction.setVersion(2);
|
||||||
|
transaction.setLocktime(AppServices.getCurrentBlockHeight() == null ? 0 : AppServices.getCurrentBlockHeight());
|
||||||
|
for(TransactionInput txInput : noFeeTransaction.getInputs()) {
|
||||||
|
transaction.addInput(txInput);
|
||||||
|
}
|
||||||
|
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
|
||||||
|
|
||||||
|
PSBT psbt = new PSBT(transaction);
|
||||||
|
for(int i = 0; i < txOutputs.size(); i++) {
|
||||||
|
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||||
|
TransactionInput txInput = transaction.getInputs().get(i);
|
||||||
|
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||||
|
psbtInput.setWitnessUtxo(utxoOutput);
|
||||||
|
|
||||||
|
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
|
||||||
|
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(txInput.getWitness() != null) {
|
||||||
|
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||||
|
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||||
|
|
||||||
|
Transaction finalizeTransaction = new Transaction();
|
||||||
|
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||||
|
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||||
|
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||||
|
}
|
||||||
|
|
||||||
|
setResult(psbt.extractTransaction());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||||
|
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||||
|
glyph.setFontSize(12);
|
||||||
|
return glyph;
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
EYE('\uf06e'),
|
EYE('\uf06e'),
|
||||||
FEATHER_ALT('\uf56b'),
|
FEATHER_ALT('\uf56b'),
|
||||||
FILE_CSV('\uf6dd'),
|
FILE_CSV('\uf6dd'),
|
||||||
|
FILE_IMPORT('\uf56f'),
|
||||||
HAND_HOLDING('\uf4bd'),
|
HAND_HOLDING('\uf4bd'),
|
||||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||||
HAND_HOLDING_WATER('\uf4c1'),
|
HAND_HOLDING_WATER('\uf4c1'),
|
||||||
|
|
|
@ -6,13 +6,13 @@ import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
@ -847,6 +847,47 @@ public class ElectrumServer {
|
||||||
return mempoolScriptHashes;
|
return mempoolScriptHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<TransactionOutput> getUtxos(Address address) throws ServerException {
|
||||||
|
Wallet wallet = new Wallet(address.toString());
|
||||||
|
Map<String, String> pathScriptHashes = new HashMap<>();
|
||||||
|
pathScriptHashes.put("m/0", getScriptHash(address));
|
||||||
|
Map<String, ScriptHashTx[]> historyResult = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
|
||||||
|
Set<String> txids = Arrays.stream(historyResult.get("m/0")).map(scriptHashTx -> scriptHashTx.tx_hash).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<String, String> transactionsResult = electrumServerRpc.getTransactions(getTransport(), wallet, txids);
|
||||||
|
List<TransactionOutput> transactionOutputs = new ArrayList<>();
|
||||||
|
Script outputScript = address.getOutputScript();
|
||||||
|
String strErrorTx = Sha256Hash.ZERO_HASH.toString();
|
||||||
|
List<Transaction> transactions = new ArrayList<>();
|
||||||
|
for(String txid : transactionsResult.keySet()) {
|
||||||
|
String strRawTx = transactionsResult.get(txid);
|
||||||
|
|
||||||
|
if(strRawTx.equals(strErrorTx)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Transaction transaction = new Transaction(Utils.hexToBytes(strRawTx));
|
||||||
|
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||||
|
if(txOutput.getScript().equals(outputScript)) {
|
||||||
|
transactionOutputs.add(txOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transactions.add(transaction);
|
||||||
|
} catch(ProtocolException e) {
|
||||||
|
log.error("Could not parse tx: " + strRawTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Transaction transaction : transactions) {
|
||||||
|
for(TransactionInput txInput : transaction.getInputs()) {
|
||||||
|
transactionOutputs.removeIf(txOutput -> txOutput.getHash().equals(txInput.getOutpoint().getHash()) && txOutput.getIndex() == txInput.getOutpoint().getIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionOutputs;
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
|
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
|
||||||
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
||||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||||
|
@ -870,6 +911,12 @@ public class ElectrumServer {
|
||||||
return Utils.bytesToHex(reversed);
|
return Utils.bytesToHex(reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getScriptHash(Address address) {
|
||||||
|
byte[] hash = Sha256Hash.hash(address.getOutputScript().getProgram());
|
||||||
|
byte[] reversed = Utils.reverseBytes(hash);
|
||||||
|
return Utils.bytesToHex(reversed);
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, List<String>> getSubscribedScriptHashes() {
|
public static Map<String, List<String>> getSubscribedScriptHashes() {
|
||||||
return subscribedScriptHashes;
|
return subscribedScriptHashes;
|
||||||
}
|
}
|
||||||
|
@ -1038,7 +1085,7 @@ public class ElectrumServer {
|
||||||
|
|
||||||
String banner = electrumServer.getServerBanner();
|
String banner = electrumServer.getServerBanner();
|
||||||
|
|
||||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||||
|
|
||||||
|
@ -1054,7 +1101,7 @@ public class ElectrumServer {
|
||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||||
if(elapsed > FEE_RATES_PERIOD) {
|
if(elapsed > FEE_RATES_PERIOD) {
|
||||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||||
|
@ -1430,7 +1477,7 @@ public class ElectrumServer {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||||
}
|
}
|
||||||
|
@ -1481,4 +1528,22 @@ public class ElectrumServer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AddressUtxosService extends Service<List<TransactionOutput>> {
|
||||||
|
private final Address address;
|
||||||
|
|
||||||
|
public AddressUtxosService(Address address) {
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<TransactionOutput>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected List<TransactionOutput> call() throws ServerException {
|
||||||
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
|
return electrumServer.getUtxos(address);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import com.google.common.eventbus.Subscribe;
|
||||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.Network;
|
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
@ -60,14 +59,11 @@ import java.text.DecimalFormatSymbols;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||||
|
|
||||||
public class SendController extends WalletFormController implements Initializable {
|
public class SendController extends WalletFormController implements Initializable {
|
||||||
private static final Logger log = LoggerFactory.getLogger(SendController.class);
|
private static final Logger log = LoggerFactory.getLogger(SendController.class);
|
||||||
|
|
||||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
|
||||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
|
||||||
|
|
||||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TabPane paymentTabs;
|
private TabPane paymentTabs;
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
|
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
|
||||||
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
|
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
|
||||||
<MenuItem fx:id="sendToMany" mnemonicParsing="false" text="Send To Many" onAction="#sendToMany"/>
|
<MenuItem fx:id="sendToMany" mnemonicParsing="false" text="Send To Many" onAction="#sendToMany"/>
|
||||||
|
<MenuItem fx:id="sweepPrivateKey" mnemonicParsing="false" text="Sweep Private Key" onAction="#sweepPrivateKey"/>
|
||||||
<SeparatorMenuItem />
|
<SeparatorMenuItem />
|
||||||
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
|
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
|
||||||
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
|
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
|
||||||
|
|
10
src/main/resources/com/sparrowwallet/sparrow/dialog.css
Normal file
10
src/main/resources/com/sparrowwallet/sparrow/dialog.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.root.dialog-pane .header-panel {
|
||||||
|
-fx-background-color: -fx-control-inner-background;
|
||||||
|
-fx-border-width: 0px 0px 1px 0px;
|
||||||
|
-fx-border-color: #e5e5e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-panel .label {
|
||||||
|
-fx-font-size: 24px;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue