mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add aopp proof of address ownership support
This commit is contained in:
parent
e5dd33d5a1
commit
425e476f20
9 changed files with 372 additions and 89 deletions
|
@ -315,7 +315,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showDocumentation(ActionEvent event) throws IOException {
|
public void showDocumentation(ActionEvent event) {
|
||||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/docs");
|
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/docs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,7 +328,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void submitBugReport(ActionEvent event) throws IOException {
|
public void submitBugReport(ActionEvent event) {
|
||||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport");
|
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1800,4 +1800,9 @@ public class AppController implements Initializable {
|
||||||
public void sendAction(SendActionEvent event) {
|
public void sendAction(SendActionEvent event) {
|
||||||
selectTab(event.getWallet());
|
selectTab(event.getWallet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void recieveAction(ReceiveActionEvent event) {
|
||||||
|
selectTab(event.getWallet());
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
|
@ -605,7 +606,7 @@ public class AppServices {
|
||||||
if("bitcoin".equals(uri.getScheme())) {
|
if("bitcoin".equals(uri.getScheme())) {
|
||||||
Platform.runLater(() -> openBitcoinUri(uri));
|
Platform.runLater(() -> openBitcoinUri(uri));
|
||||||
} else if("aopp".equals(uri.getScheme())) {
|
} else if("aopp".equals(uri.getScheme())) {
|
||||||
log.info(uri.toString());
|
Platform.runLater(() -> openAddressOwnershipProof(uri));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -613,25 +614,7 @@ public class AppServices {
|
||||||
private static void openBitcoinUri(URI uri) {
|
private static void openBitcoinUri(URI uri) {
|
||||||
try {
|
try {
|
||||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||||
|
Wallet wallet = selectWallet(null, "pay from");
|
||||||
Wallet wallet = null;
|
|
||||||
Set<Wallet> wallets = get().getOpenWallets().keySet();
|
|
||||||
if(wallets.isEmpty()) {
|
|
||||||
showErrorDialog("No wallet available", "Open a wallet to send to the provided bitcoin URI.");
|
|
||||||
} else if(wallets.size() == 1) {
|
|
||||||
wallet = wallets.iterator().next();
|
|
||||||
} else {
|
|
||||||
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
|
||||||
walletChoiceDialog.setTitle("Choose Wallet");
|
|
||||||
walletChoiceDialog.setHeaderText("Choose a wallet to pay from");
|
|
||||||
Image image = new Image("/image/sparrow-small.png");
|
|
||||||
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
|
||||||
AppServices.setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
|
||||||
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
|
||||||
if(optWallet.isPresent()) {
|
|
||||||
wallet = optWallet.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
final Wallet sendingWallet = wallet;
|
final Wallet sendingWallet = wallet;
|
||||||
|
@ -643,6 +626,44 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openAddressOwnershipProof(URI uri) {
|
||||||
|
try {
|
||||||
|
Aopp aopp = new Aopp(uri);
|
||||||
|
Wallet wallet = selectWallet(aopp.getScriptType(), "send proof of address");
|
||||||
|
|
||||||
|
if(wallet != null) {
|
||||||
|
EventManager.get().post(new ReceiveActionEvent(wallet));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new ReceiveProofEvent(wallet, aopp)));
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
showErrorDialog("Not a valid AOPP URI", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
||||||
|
Wallet wallet = null;
|
||||||
|
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
||||||
|
if(wallets.isEmpty()) {
|
||||||
|
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
||||||
|
} else if(wallets.size() == 1) {
|
||||||
|
wallet = wallets.iterator().next();
|
||||||
|
} else {
|
||||||
|
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
||||||
|
walletChoiceDialog.setTitle("Choose Wallet");
|
||||||
|
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
||||||
|
Image image = new Image("/image/sparrow-small.png");
|
||||||
|
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
||||||
|
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
||||||
|
moveToActiveWindowScreen(walletChoiceDialog);
|
||||||
|
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
||||||
|
if(optWallet.isPresent()) {
|
||||||
|
wallet = optWallet.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
public static Font getMonospaceFont() {
|
public static Font getMonospaceFont() {
|
||||||
return Font.font("Roboto Mono", 13);
|
return Font.font("Roboto Mono", 13);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import tornadofx.control.Fieldset;
|
||||||
import tornadofx.control.Form;
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
private final TextArea signature;
|
private final TextArea signature;
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
private WalletNode walletNode;
|
private WalletNode walletNode;
|
||||||
|
private boolean electrumSignatureFormat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verification only constructor
|
* Verification only constructor
|
||||||
|
@ -69,6 +71,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
* @param walletNode Wallet node to derive address from
|
* @param walletNode Wallet node to derive address from
|
||||||
*/
|
*/
|
||||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
|
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
|
||||||
|
this(wallet, walletNode, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign and verify with preset address, and supplied title, message and dialog buttons
|
||||||
|
*
|
||||||
|
* @param wallet Wallet to sign with
|
||||||
|
* @param walletNode Wallet node to derive address from
|
||||||
|
* @param title Header text of dialog
|
||||||
|
* @param msg Message to sign (all fields will be made uneditable)
|
||||||
|
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
|
||||||
|
*/
|
||||||
|
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
if(wallet.getKeystores().size() != 1) {
|
if(wallet.getKeystores().size() != 1) {
|
||||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||||
|
@ -84,7 +99,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());
|
||||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
|
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||||
|
|
||||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||||
if (!image.isError()) {
|
if (!image.isError()) {
|
||||||
|
@ -133,47 +148,70 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
form.getChildren().add(fieldset);
|
form.getChildren().add(fieldset);
|
||||||
dialogPane.setContent(form);
|
dialogPane.setContent(form);
|
||||||
|
|
||||||
|
if(msg != null) {
|
||||||
|
message.setText(msg);
|
||||||
|
address.setEditable(false);
|
||||||
|
message.setEditable(false);
|
||||||
|
signature.setEditable(false);
|
||||||
|
}
|
||||||
|
|
||||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||||
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
|
||||||
|
|
||||||
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
|
if(buttons.length > 0) {
|
||||||
signButton.setDisable(wallet == null);
|
dialogPane.getButtonTypes().addAll(buttons);
|
||||||
signButton.setOnAction(event -> {
|
|
||||||
signMessage();
|
|
||||||
});
|
|
||||||
|
|
||||||
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
|
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
|
||||||
verifyButton.setDefaultButton(false);
|
if(customSignButtonType != null) {
|
||||||
verifyButton.setOnAction(event -> {
|
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
|
||||||
verifyMessage();
|
customSignButton.setDefaultButton(true);
|
||||||
});
|
customSignButton.setOnAction(event -> {
|
||||||
|
customSignButton.setDisable(true);
|
||||||
boolean validAddress = isValidAddress();
|
signMessage();
|
||||||
signButton.setDisable(!validAddress || (wallet == null));
|
setResult(ButtonBar.ButtonData.OK_DONE);
|
||||||
verifyButton.setDisable(!validAddress);
|
});
|
||||||
|
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
|
||||||
});
|
|
||||||
|
|
||||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
|
||||||
boolean valid = isValidAddress();
|
|
||||||
signButton.setDisable(!valid || (wallet == null));
|
|
||||||
verifyButton.setDisable(!valid);
|
|
||||||
|
|
||||||
if(valid && wallet != null) {
|
|
||||||
try {
|
|
||||||
Address address = getAddress();
|
|
||||||
setWalletNodeFromAddress(wallet, address);
|
|
||||||
} catch(InvalidAddressException e) {
|
|
||||||
//can't happen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||||
|
|
||||||
|
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||||
|
signButton.setDisable(wallet == null);
|
||||||
|
signButton.setOnAction(event -> {
|
||||||
|
signMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button verifyButton = (Button) dialogPane.lookupButton(verifyButtonType);
|
||||||
|
verifyButton.setDefaultButton(false);
|
||||||
|
verifyButton.setOnAction(event -> {
|
||||||
|
verifyMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean validAddress = isValidAddress();
|
||||||
|
signButton.setDisable(!validAddress || (wallet == null));
|
||||||
|
verifyButton.setDisable(!validAddress);
|
||||||
|
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
});
|
||||||
|
|
||||||
|
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
boolean valid = isValidAddress();
|
||||||
|
signButton.setDisable(!valid || (wallet == null));
|
||||||
|
verifyButton.setDisable(!valid);
|
||||||
|
|
||||||
|
if(valid && wallet != null) {
|
||||||
|
try {
|
||||||
|
Address address = getAddress();
|
||||||
|
setWalletNodeFromAddress(wallet, address);
|
||||||
|
} catch(InvalidAddressException e) {
|
||||||
|
//can't happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
setOnCloseRequest(event -> {
|
setOnCloseRequest(event -> {
|
||||||
|
@ -186,13 +224,26 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
});
|
});
|
||||||
|
|
||||||
AppServices.moveToActiveWindowScreen(this);
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
|
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Address getAddress()throws InvalidAddressException {
|
private Address getAddress()throws InvalidAddressException {
|
||||||
return Address.fromString(address.getText());
|
return Address.fromString(address.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSignature() {
|
||||||
|
return signature.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the Electrum signing format, which uses the non-segwit compressed signing parameters for both segwit types (p2sh-p2wpkh and p2wpkh)
|
||||||
|
*
|
||||||
|
* @param electrumSignatureFormat
|
||||||
|
*/
|
||||||
|
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
|
||||||
|
this.electrumSignatureFormat = electrumSignatureFormat;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isValidAddress() {
|
private boolean isValidAddress() {
|
||||||
try {
|
try {
|
||||||
getAddress();
|
getAddress();
|
||||||
|
@ -228,7 +279,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
//Note we can expect a single keystore due to the check above
|
//Note we can expect a single keystore due to the check above
|
||||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||||
ECKey privKey = keystore.getKey(walletNode);
|
ECKey privKey = keystore.getKey(walletNode);
|
||||||
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
|
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||||
|
String signatureText = privKey.signMessage(message.getText().trim(), scriptType, null);
|
||||||
signature.clear();
|
signature.clear();
|
||||||
signature.appendText(signatureText);
|
signature.appendText(signatureText);
|
||||||
privKey.clear();
|
privKey.clear();
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
|
|
||||||
public class ReceiveActionEvent {
|
public class ReceiveActionEvent {
|
||||||
private final NodeEntry receiveEntry;
|
private final Wallet wallet;
|
||||||
|
|
||||||
public ReceiveActionEvent(NodeEntry receiveEntry) {
|
public ReceiveActionEvent(NodeEntry receiveEntry) {
|
||||||
this.receiveEntry = receiveEntry;
|
this.wallet = receiveEntry.getWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public NodeEntry getReceiveEntry() {
|
public ReceiveActionEvent(Wallet wallet) {
|
||||||
return receiveEntry;
|
this.wallet = wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.net.Aopp;
|
||||||
|
|
||||||
|
public class ReceiveProofEvent {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final Aopp aopp;
|
||||||
|
|
||||||
|
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.aopp = aopp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Aopp getAopp() {
|
||||||
|
return aopp;
|
||||||
|
}
|
||||||
|
}
|
167
src/main/java/com/sparrowwallet/sparrow/net/Aopp.java
Normal file
167
src/main/java/com/sparrowwallet/sparrow/net/Aopp.java
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Aopp {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Aopp.class);
|
||||||
|
|
||||||
|
public static final String SCHEME = "aopp";
|
||||||
|
|
||||||
|
private final int version;
|
||||||
|
private final String message;
|
||||||
|
private final ScriptType scriptType;
|
||||||
|
private final URL callback;
|
||||||
|
|
||||||
|
public Aopp(URI uri) throws MalformedURLException {
|
||||||
|
if(!uri.getScheme().equals(SCHEME)) {
|
||||||
|
throw new IllegalArgumentException("Uri " + uri + " does not have the correct scheme");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||||
|
String query = uri.getSchemeSpecificPart();
|
||||||
|
if(query.startsWith("?")) {
|
||||||
|
query = query.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] pairs = query.split("&");
|
||||||
|
for(String pair : pairs) {
|
||||||
|
int idx = pair.indexOf("=");
|
||||||
|
parameterMap.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String strVersion = parameterMap.get("v");
|
||||||
|
if(strVersion == null) {
|
||||||
|
throw new IllegalArgumentException("No version parameter provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
version = Integer.parseInt(strVersion);
|
||||||
|
if(version != 0) {
|
||||||
|
throw new IllegalArgumentException("Unsupported version number " + version);
|
||||||
|
}
|
||||||
|
|
||||||
|
String strMessage = parameterMap.get("msg");
|
||||||
|
if(strMessage == null) {
|
||||||
|
throw new IllegalArgumentException("No msg parameter provided");
|
||||||
|
}
|
||||||
|
message = strMessage.replace('+', ' ');
|
||||||
|
|
||||||
|
String asset = parameterMap.get("asset");
|
||||||
|
if(asset == null || !asset.equals("btc")) {
|
||||||
|
throw new IllegalArgumentException("Unsupported asset type " + asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
String format = parameterMap.get("format");
|
||||||
|
if(format == null) {
|
||||||
|
throw new IllegalArgumentException("No format parameter provided");
|
||||||
|
}
|
||||||
|
if(format.equals("p2sh")) {
|
||||||
|
format = "p2sh_p2wpkh";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(format.equals("any")) {
|
||||||
|
scriptType = null;
|
||||||
|
} else {
|
||||||
|
scriptType = ScriptType.valueOf(format.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
String callbackUrl = parameterMap.get("callback");
|
||||||
|
if(callbackUrl == null) {
|
||||||
|
throw new IllegalArgumentException("No callback parameter provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback = new URL(callbackUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendProofOfAddress(Address address, String signature) throws URISyntaxException, IOException, InterruptedException, AoppException {
|
||||||
|
Proxy proxy = AppServices.getProxy();
|
||||||
|
|
||||||
|
CallbackRequest callbackRequest = new CallbackRequest(version, address.toString(), signature);
|
||||||
|
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||||
|
String json = gson.toJson(callbackRequest);
|
||||||
|
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug("Sending " + json + " to " + callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection connection = proxy == null ? (HttpURLConnection)callback.openConnection() : (HttpURLConnection)callback.openConnection(proxy);
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
|
try(OutputStream os = connection.getOutputStream()) {
|
||||||
|
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(jsonBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String responseLine;
|
||||||
|
while((responseLine = br.readLine()) != null) {
|
||||||
|
response.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int statusCode = connection.getResponseCode();
|
||||||
|
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug("Received " + statusCode + " " + response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new AoppException("Could not send proof of ownership. Server returned " + response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptType getScriptType() {
|
||||||
|
return scriptType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URL getCallback() {
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CallbackRequest {
|
||||||
|
public CallbackRequest(int version, String address, String signature) {
|
||||||
|
this.version = version;
|
||||||
|
this.address = address;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int version;
|
||||||
|
public String address;
|
||||||
|
public String signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class AoppException extends Exception {
|
||||||
|
public AoppException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AoppException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AoppException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AoppException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -57,7 +55,7 @@ public enum FeeRatesSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||||
Proxy proxy = getProxy();
|
Proxy proxy = AppServices.getProxy();
|
||||||
|
|
||||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||||
|
@ -97,17 +95,6 @@ public enum FeeRatesSource {
|
||||||
return blockTargetFeeRates;
|
return blockTargetFeeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Proxy getProxy() {
|
|
||||||
Config config = Config.get();
|
|
||||||
if(config.isUseProxy()) {
|
|
||||||
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
|
|
||||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
|
||||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.google.zxing.common.BitMatrix;
|
||||||
import com.google.zxing.qrcode.QRCodeWriter;
|
import com.google.zxing.qrcode.QRCodeWriter;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
@ -19,17 +20,14 @@ import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.Device;
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
import com.sparrowwallet.sparrow.io.Hwi;
|
import com.sparrowwallet.sparrow.io.Hwi;
|
||||||
|
import com.sparrowwallet.sparrow.net.Aopp;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.fxmisc.richtext.CodeArea;
|
import org.fxmisc.richtext.CodeArea;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -40,10 +38,7 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ReceiveController extends WalletFormController implements Initializable {
|
public class ReceiveController extends WalletFormController implements Initializable {
|
||||||
|
@ -248,6 +243,28 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||||
this.currentEntry = null;
|
this.currentEntry = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void signAndSendProofOfAddress(Aopp aopp) {
|
||||||
|
if(currentEntry == null) {
|
||||||
|
Platform.runLater(() -> signAndSendProofOfAddress(aopp));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
ButtonType signSendButtonType = new ButtonType("Sign & Send", ButtonBar.ButtonData.APPLY);
|
||||||
|
ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
MessageSignDialog messageSignDialog = new MessageSignDialog(getWalletForm().getWallet(), currentEntry.getNode(), "Send Proof of Address", aopp.getMessage(), signSendButtonType, cancelButtonType);
|
||||||
|
messageSignDialog.setElectrumSignatureFormat(true);
|
||||||
|
Optional<ButtonBar.ButtonData> buttonData = messageSignDialog.showAndWait();
|
||||||
|
if(buttonData.isPresent() && buttonData.get() == ButtonBar.ButtonData.OK_DONE) {
|
||||||
|
Address address = getWalletForm().getWallet().getAddress(currentEntry.getNode());
|
||||||
|
String signature = messageSignDialog.getSignature();
|
||||||
|
aopp.sendProofOfAddress(address, signature);
|
||||||
|
AppServices.showAlertDialog("Proof of Address Sent", "Proof of ownership of address " + address + " has been successfully sent to " + aopp.getCallback().getHost() + ".", Alert.AlertType.INFORMATION);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
AppServices.showErrorDialog("Cannot send proof of ownership", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Glyph getUnusedGlyph() {
|
public static Glyph getUnusedGlyph() {
|
||||||
Glyph checkGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
Glyph checkGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||||
checkGlyph.getStyleClass().add("unused-check");
|
checkGlyph.getStyleClass().add("unused-check");
|
||||||
|
@ -280,6 +297,13 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void receiveProof(ReceiveProofEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
Platform.runLater(() -> signAndSendProofOfAddress(event.getAopp()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||||
|
|
|
@ -114,7 +114,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void receiveAction(ReceiveActionEvent event) {
|
public void receiveAction(ReceiveActionEvent event) {
|
||||||
if(event.getReceiveEntry().getWallet().equals(walletForm.getWallet())) {
|
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||||
selectFunction(Function.RECEIVE);
|
selectFunction(Function.RECEIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue