mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 20:16:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67e1573329 | ||
|
|
23a9db5fc6 | ||
|
|
e9108b85cd | ||
|
|
3766585474 | ||
|
|
0e7aa34d07 | ||
|
|
537c2ffef0 | ||
|
|
8b4f388eb7 | ||
|
|
6a07adfea5 | ||
|
|
eeade6a173 | ||
|
|
4c2b8055ec | ||
|
|
04d210ed4b | ||
|
|
5b737be76d | ||
|
|
908fb80d2a | ||
|
|
eda7087bdd | ||
|
|
a5d61f1a9f | ||
|
|
2621bb6504 | ||
|
|
7dfc432bd2 | ||
|
|
df5b5146ba | ||
|
|
452584d87d | ||
|
|
5c823533d3 | ||
|
|
6561971d4f | ||
|
|
8ebf8dc041 | ||
|
|
419e8e1374 | ||
|
|
684965cf89 | ||
|
|
3498b5bab1 | ||
|
|
74635b1d1f | ||
|
|
229c6430ed | ||
|
|
c74abfeeb7 | ||
|
|
c78bc7b1cf | ||
|
|
87cd96a627 | ||
|
|
7ae04a2564 | ||
|
|
d4c0df0df9 | ||
|
|
dc88dc4ede | ||
|
|
a478edfad7 | ||
|
|
46d444615c | ||
|
|
78fe55787b | ||
|
|
cd0be365de | ||
|
|
b9d62c0b7b |
+19
-30
@@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.4.1'
|
||||
version = '2.4.2'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -45,24 +45,24 @@ dependencies {
|
||||
implementation(project(':drongo'))
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.5.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation('com.google.code.gson:gson:2.13.2')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||
implementation('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-core:3.49.5') {
|
||||
implementation('org.jdbi:jdbi3-core:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
implementation('com.google.zxing:javase:3.5.4') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('org.jcommander:jcommander:2.0')
|
||||
implementation('org.jcommander:jcommander:3.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
@@ -70,19 +70,19 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
implementation('org.controlsfx:controlsfx:11.2.3' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
||||
@@ -93,26 +93,25 @@ dependencies {
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||
implementation ('org.slf4j:slf4j-api:2.0.17')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('com.sparrowwallet:tern:1.0.6')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.19.0')
|
||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('org.apache.commons:commons-lang3:3.20.0')
|
||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.43')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('net.coobird:thumbnailator:0.4.21')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
implementation('io.github.doblon8:jzbar:0.3.0')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
}
|
||||
|
||||
@@ -462,16 +461,6 @@ extraJavaModuleInfo {
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="2.4.0"
|
||||
GIT_TAG="2.4.1"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: 0a500ea002...4049ebcdda
+1
-1
Submodule lark updated: b074099a38...7f79ddee6b
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.4.1</string>
|
||||
<string>2.4.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -328,6 +328,9 @@ public class AppServices {
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile == null) {
|
||||
crtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
|
||||
}
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "2.4.1";
|
||||
public static final String APP_VERSION = "2.4.2";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -10,7 +10,12 @@ import com.sparrowwallet.drongo.crypto.Bip322;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -242,7 +247,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(wallet != null) {
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
if(walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
}
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
@@ -250,6 +255,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
|
||||
if(wallet != null) {
|
||||
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
|
||||
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventManager.get().register(this);
|
||||
@@ -277,7 +289,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
if(wallet != null && walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
} else {
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
}
|
||||
@@ -296,7 +308,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||
}
|
||||
|
||||
private boolean canSignBip322(Wallet wallet) {
|
||||
private boolean canSignAllFormats(Wallet wallet) {
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||
}
|
||||
|
||||
@@ -321,11 +333,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
walletNode = wallet.getWalletAddresses().get(address);
|
||||
}
|
||||
|
||||
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||
}
|
||||
|
||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||
@@ -473,6 +480,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
showBip322Qr();
|
||||
return;
|
||||
}
|
||||
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
|
||||
@@ -486,13 +498,57 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void showBip322Qr() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
byte[] psbtBytes = psbt.serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
|
||||
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
Keystore keystore = signingWallet.getKeystores().get(0);
|
||||
ECKey pubKey = keystore.getPubKey(walletNode);
|
||||
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
|
||||
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
psbtInput.setTapInternalKey(pubKey);
|
||||
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
|
||||
} else {
|
||||
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(pubKey), fullDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
private void scanQr() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.payload != null) {
|
||||
if(result.psbt != null) {
|
||||
try {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ECKey pubKey = signingWallet.getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), result.psbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
} catch(Exception e) {
|
||||
log.error("Error extracting BIP-322 signature from PSBT", e);
|
||||
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
signature.clear();
|
||||
signature.appendText(result.payload);
|
||||
} else if(result.exception != null) {
|
||||
@@ -510,6 +566,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
exportBip322File();
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
@@ -538,20 +599,66 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportBip322File() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PSBT File");
|
||||
fileChooser.setInitialFileName("bip322-signmessage.psbt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(OutputStream os = new FileOutputStream(file)) {
|
||||
os.write(psbt.serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving BIP-322 PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed Text File");
|
||||
fileChooser.setTitle("Open Signed File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
|
||||
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] psbtBytes = Files.readAllBytes(file.toPath());
|
||||
PSBT signedPsbt = new PSBT(psbtBytes, false);
|
||||
ECKey pubKey = walletNode.getWallet().getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(walletNode.getWallet().getScriptType(), signedPsbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
log.error("Error loading signed PSBT", e);
|
||||
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
//Fall through to text handling for non-.psbt files
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
|
||||
@@ -37,7 +37,8 @@ public class UsbStatusButton extends MenuButton {
|
||||
togglePassphraseService.setOnSucceeded(event1 -> {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
if(!device.getModel().externalPassphraseEntry()) {
|
||||
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
|
||||
AppServices.showAlertDialog("Restart device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase." +
|
||||
"\n\nIf it has a battery, hold down the power button until it restarts.", Alert.AlertType.INFORMATION);
|
||||
}
|
||||
});
|
||||
togglePassphraseService.setOnFailed(event1 -> {
|
||||
|
||||
@@ -73,7 +73,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
name = (CustomTextField)TextFields.createClearableTextField();
|
||||
name.setText(initialName);
|
||||
name.setTextFormatter(new TextFormatter<>((change) -> {
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|;]", "_"));
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|;`]", "_"));
|
||||
return change;
|
||||
}));
|
||||
content.getChildren().add(name);
|
||||
|
||||
@@ -114,26 +114,14 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
|
||||
private static List<String> getParagraphs(InputStream inputStream) {
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
StringBuilder paragraph = new StringBuilder();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
|
||||
if(line.isEmpty()) {
|
||||
if(!paragraph.isEmpty()) {
|
||||
paragraphs.add(paragraph.toString());
|
||||
paragraph.setLength(0);
|
||||
}
|
||||
} else if(line.startsWith("#")) {
|
||||
continue;
|
||||
} else {
|
||||
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
||||
if(!line.isEmpty() && !line.startsWith("#")) {
|
||||
paragraphs.add(line.replaceFirst("^.+:", "").trim());
|
||||
}
|
||||
}
|
||||
|
||||
if(!paragraph.isEmpty()) {
|
||||
paragraphs.add(paragraph.toString());
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -503,8 +504,23 @@ public class Storage {
|
||||
}
|
||||
|
||||
public static File getCertificateFile(String host) {
|
||||
File certsDir = getCertsDir();
|
||||
File[] certs = certsDir.listFiles((dir, name) -> name.equals(host));
|
||||
return findCertFile(getCertName(host));
|
||||
}
|
||||
|
||||
public static void saveCertificate(String host, Certificate cert) {
|
||||
writeCertPem(getCertName(host), cert);
|
||||
}
|
||||
|
||||
public static File getCaCertificateFile(String host) {
|
||||
return findCertFile(host + ".cacert");
|
||||
}
|
||||
|
||||
public static void saveCaCertificate(String host, Certificate cert) {
|
||||
writeCertPem(host + ".cacert", cert);
|
||||
}
|
||||
|
||||
private static File findCertFile(String filename) {
|
||||
File[] certs = getCertsDir().listFiles((dir, name) -> name.equals(filename));
|
||||
if(certs != null && certs.length > 0) {
|
||||
return certs[0];
|
||||
}
|
||||
@@ -512,8 +528,8 @@ public class Storage {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void saveCertificate(String host, Certificate cert) {
|
||||
try(FileWriter writer = new FileWriter(new File(getCertsDir(), host))) {
|
||||
private static void writeCertPem(String filename, Certificate cert) {
|
||||
try(FileWriter writer = new FileWriter(new File(getCertsDir(), filename))) {
|
||||
writer.write("-----BEGIN CERTIFICATE-----\n");
|
||||
writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n"));
|
||||
writer.write("\n-----END CERTIFICATE-----\n");
|
||||
@@ -524,6 +540,14 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
private static String getCertName(String host) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
return host + ".bitcoind";
|
||||
}
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
static File getCertsDir() {
|
||||
File certsDir = new File(getSparrowDir(), CERTS_DIR);
|
||||
if(!certsDir.exists()) {
|
||||
|
||||
@@ -83,6 +83,7 @@ public class DbPersistence implements Persistence {
|
||||
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
|
||||
|
||||
migrate(storage, MASTER_SCHEMA, encryptionKey);
|
||||
validateSchema(storage, MASTER_SCHEMA, encryptionKey);
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
masterWallet = jdbi.withHandle(handle -> {
|
||||
@@ -112,6 +113,7 @@ public class DbPersistence implements Persistence {
|
||||
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
|
||||
for(String schema : childSchemas) {
|
||||
migrate(storage, schema, encryptionKey);
|
||||
validateSchema(storage, schema, encryptionKey);
|
||||
|
||||
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
Wallet wallet = childJdbi.withHandle(handle -> {
|
||||
@@ -160,11 +162,20 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException {
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
String newPassword = getFilePassword(encryptionPubKey);
|
||||
String currentPassword = getDatasourcePassword();
|
||||
|
||||
updateExecutor.execute(() -> {
|
||||
try {
|
||||
update(storage, wallet, getFilePassword(encryptionPubKey));
|
||||
if(dataSource != null && currentPassword != null && newPassword == null) {
|
||||
//Removing encryption: write data first
|
||||
update(storage, wallet, currentPassword);
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
} else {
|
||||
//Adding encryption or no change: change file first
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
update(storage, wallet, newPassword);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error updating wallet db", e);
|
||||
}
|
||||
@@ -215,7 +226,7 @@ public class DbPersistence implements Persistence {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
|
||||
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
|
||||
handle.execute("drop schema `" + getSchema(wallet).replace("`", "``") + "` cascade");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -409,6 +420,52 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSchema(Storage storage, String schema, ECKey encryptionKey) throws StorageException {
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
try {
|
||||
jdbi.useHandle(handle -> {
|
||||
List<String> routines = handle.createQuery("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
|
||||
if(!routines.isEmpty()) {
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected database routines: " + String.join(", ", routines) + "."));
|
||||
}
|
||||
|
||||
List<String> triggers = handle.createQuery("SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
|
||||
if(!triggers.isEmpty()) {
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected database triggers: " + String.join(", ", triggers) + "."));
|
||||
}
|
||||
|
||||
List<String> checkConstraints = handle.createQuery("SELECT CHECK_CLAUSE FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS WHERE UPPER(CONSTRAINT_SCHEMA) = UPPER(:schema)")
|
||||
.bind("schema", schema).mapTo(String.class).list();
|
||||
if(!checkConstraints.isEmpty()) {
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected check constraints: " + String.join(", ", checkConstraints) + "."));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> nonBaseTables = handle.createQuery("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) "
|
||||
+ "AND TABLE_TYPE <> 'BASE TABLE' AND UPPER(TABLE_NAME) <> 'FLYWAY_SCHEMA_HISTORY'").bind("schema", schema).mapToMap().list();
|
||||
if(!nonBaseTables.isEmpty()) {
|
||||
String detail = nonBaseTables.stream().map(m -> m.get("TABLE_NAME") + " (" + m.get("TABLE_TYPE") + ")").collect(Collectors.joining(", "));
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected database object types: " + detail + "."));
|
||||
}
|
||||
|
||||
List<String> generatedColumns = handle.createQuery("SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) AND GENERATION_EXPRESSION IS NOT NULL")
|
||||
.bind("schema", schema).mapTo(String.class).list();
|
||||
if(!generatedColumns.isEmpty()) {
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected generated columns: " + String.join(", ", generatedColumns) + "."));
|
||||
}
|
||||
|
||||
List<String> domains = handle.createQuery("SELECT DOMAIN_NAME FROM INFORMATION_SCHEMA.DOMAINS WHERE DOMAIN_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
|
||||
if(!domains.isEmpty()) {
|
||||
throw new RuntimeException(new StorageException("Wallet file contains unexpected database domains: " + String.join(", ", domains) + "."));
|
||||
}
|
||||
});
|
||||
} catch(RuntimeException e) {
|
||||
if(e.getCause() instanceof StorageException) {
|
||||
throw new StorageException("This is not a valid wallet file.\n\n" + e.getCause().getMessage());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException {
|
||||
File migrationDir = getMigrationDir();
|
||||
try {
|
||||
@@ -698,8 +755,8 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
|
||||
private String getUrl(File walletFile, String password) throws StorageException {
|
||||
if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getName()).find()) {
|
||||
throw new StorageException("Wallet file name contains invalid characters");
|
||||
if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getAbsolutePath()).find()) {
|
||||
throw new StorageException("Wallet file path contains invalid characters");
|
||||
}
|
||||
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
@@ -1435,6 +1436,12 @@ public class ElectrumServer {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
}
|
||||
} catch(IllegalStateException e) {
|
||||
if(e.getCause() instanceof SSLHandshakeException) {
|
||||
throw new TlsServerException(Config.get().getCoreServer().getHostAndPort(), e.getCause());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||
InetSocketAddress proxyAddr = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT));
|
||||
socket = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
|
||||
socket.connect(InetSocketAddress.createUnresolved(server.getHost(), server.getPortOrDefault(getDefaultPort())));
|
||||
socket = sslSocketFactory.createSocket(socket, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true);
|
||||
socket = sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(getDefaultPort()), true);
|
||||
startHandshake((SSLSocket)socket);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -19,11 +20,19 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
public static final int PAD_TO_MULTIPLE_OF_BYTES = 96;
|
||||
|
||||
protected final SSLSocketFactory sslSocketFactory;
|
||||
protected final boolean usingCaTrust;
|
||||
|
||||
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException {
|
||||
super(server);
|
||||
|
||||
TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()));
|
||||
TrustManager[] trustManagers;
|
||||
if(Storage.getCaCertificateFile(server.getHost()) != null) {
|
||||
trustManagers = getCaTrustManagers();
|
||||
this.usingCaTrust = true;
|
||||
} else {
|
||||
trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()), server.getHost());
|
||||
this.usingCaTrust = false;
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagers, new SecureRandom());
|
||||
@@ -34,7 +43,8 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
super(server);
|
||||
|
||||
TrustManager[] trustManagers = getTrustManagers(crtFile);
|
||||
this.usingCaTrust = false;
|
||||
TrustManager[] trustManagers = getTrustManagers(crtFile, server.getHost());
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagers, null);
|
||||
@@ -60,7 +70,7 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
|
||||
public static TrustManager[] getTrustManagers(File crtFile, String host) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
|
||||
if(crtFile == null) {
|
||||
return new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
@@ -79,7 +89,7 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
try {
|
||||
certs[0].checkValidity();
|
||||
} catch(CertificateExpiredException e) {
|
||||
if(Storage.getCertificateFile(server.getHost()) == null) {
|
||||
if(Storage.getCertificateFile(host) == null) {
|
||||
throw new UnknownCertificateExpiredException(e.getMessage(), certs[0]);
|
||||
}
|
||||
}
|
||||
@@ -88,17 +98,23 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
};
|
||||
}
|
||||
|
||||
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));
|
||||
Certificate certificate;
|
||||
try(FileInputStream fis = new FileInputStream(crtFile)) {
|
||||
certificate = CertificateFactory.getInstance("X.509").generateCertificate(fis);
|
||||
}
|
||||
if(certificate instanceof X509Certificate) {
|
||||
try {
|
||||
X509Certificate x509Certificate = (X509Certificate)certificate;
|
||||
x509Certificate.checkValidity();
|
||||
} catch(CertificateExpiredException e) {
|
||||
//Allow expired certificates so long as they have been previously used or explicitly approved
|
||||
//These will usually be self-signed certificates that users may not have the expertise to renew
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
crtFile.delete();
|
||||
return getTrustManagers(null, host);
|
||||
}
|
||||
//Allow expired certificates for private servers where users may not have the expertise to renew
|
||||
} catch(CertificateException e) {
|
||||
crtFile.delete();
|
||||
return getTrustManagers(null);
|
||||
return getTrustManagers(null, host);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +140,11 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
try {
|
||||
Certificate[] certs = event.getPeerCertificates();
|
||||
if(certs.length > 0) {
|
||||
Storage.saveCertificate(server.getHost(), certs[0]);
|
||||
if(isCaSigned(certs)) {
|
||||
Storage.saveCaCertificate(server.getHost(), certs[0]);
|
||||
} else {
|
||||
Storage.saveCertificate(server.getHost(), certs[0]);
|
||||
}
|
||||
}
|
||||
} catch(SSLPeerUnverifiedException e) {
|
||||
log.warn("Attempting to retrieve certificate for unverified peer", e);
|
||||
@@ -132,18 +152,53 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
}
|
||||
});
|
||||
|
||||
if(usingCaTrust && !Protocol.isOnionAddress(server)) {
|
||||
SSLParameters sslParameters = sslSocket.getSSLParameters();
|
||||
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
|
||||
sslSocket.setSSLParameters(sslParameters);
|
||||
}
|
||||
|
||||
sslSocket.startHandshake();
|
||||
}
|
||||
|
||||
protected boolean shouldSaveCertificate() {
|
||||
//Avoid saving the certificates for public servers - they change often, encourage approval complacency, and there is little a user can do to check
|
||||
for(PublicElectrumServer publicElectrumServer : PublicElectrumServer.getServers()) {
|
||||
if(publicElectrumServer.getServer().getHost().equals(server.getHost())) {
|
||||
return Storage.getCertificateFile(server.getHost()) == null && Storage.getCaCertificateFile(server.getHost()) == null;
|
||||
}
|
||||
|
||||
private static TrustManager[] getCaTrustManagers() throws NoSuchAlgorithmException, KeyStoreException {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore)null);
|
||||
return tmf.getTrustManagers();
|
||||
}
|
||||
|
||||
private static boolean isCaSigned(Certificate[] certs) {
|
||||
try {
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore)null);
|
||||
|
||||
X509TrustManager defaultTm = null;
|
||||
for(TrustManager tm : tmf.getTrustManagers()) {
|
||||
if(tm instanceof X509TrustManager) {
|
||||
defaultTm = (X509TrustManager)tm;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(defaultTm == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return Storage.getCertificateFile(server.getHost()) == null;
|
||||
X509Certificate[] x509Certs = new X509Certificate[certs.length];
|
||||
for(int i = 0; i < certs.length; i++) {
|
||||
x509Certs[i] = (X509Certificate)certs[i];
|
||||
}
|
||||
|
||||
String authType = x509Certs[0].getPublicKey().getAlgorithm().equals("EC") ? "ECDHE_ECDSA" : "RSA";
|
||||
defaultTm.checkServerTrusted(x509Certs, authType);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,6 +42,8 @@ public class TlsServerException extends ServerException {
|
||||
return "Provided server certificate from " + server.getHost() + " did not match configured certificate at " + configCrtFile.getAbsolutePath();
|
||||
} else if(savedCrtFile != null) {
|
||||
return "Provided server certificate from " + server.getHost() + " did not match previously saved certificate at " + savedCrtFile.getAbsolutePath();
|
||||
} else if(Storage.getCaCertificateFile(server.getHost()) != null) {
|
||||
return "Provided server certificate from " + server.getHost() + " failed CA validation";
|
||||
}
|
||||
|
||||
return "Provided server certificate from " + server.getHost() + " was invalid: " + (cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage());
|
||||
|
||||
+36
-25
@@ -4,7 +4,9 @@ import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
import com.sparrowwallet.sparrow.net.TcpOverTlsTransport;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -14,8 +16,7 @@ import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BitcoindTransport implements Transport {
|
||||
@@ -27,6 +28,7 @@ public class BitcoindTransport implements Transport {
|
||||
private File cookieFile;
|
||||
private Long cookieFileTimestamp;
|
||||
private String bitcoindAuthEncoded;
|
||||
private SSLSocketFactory sslSocketFactory;
|
||||
|
||||
public BitcoindTransport(Server bitcoindServer, String bitcoindWallet, String bitcoindAuth) {
|
||||
this(bitcoindServer, bitcoindWallet);
|
||||
@@ -57,9 +59,10 @@ public class BitcoindTransport implements Transport {
|
||||
HttpURLConnection connection = proxy != null && Protocol.isOnionAddress(bitcoindServer) ? (HttpURLConnection)bitcoindUrl.openConnection(proxy) : (HttpURLConnection)bitcoindUrl.openConnection();
|
||||
|
||||
if(connection instanceof HttpsURLConnection httpsURLConnection) {
|
||||
SSLSocketFactory sslSocketFactory = getTrustAllSocketFactory();
|
||||
SSLSocketFactory sslSocketFactory = getSSLSocketFactory();
|
||||
if(sslSocketFactory != null) {
|
||||
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
|
||||
httpsURLConnection.setHostnameVerifier((_, _) -> true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +84,19 @@ public class BitcoindTransport implements Transport {
|
||||
}
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
|
||||
if(connection instanceof HttpsURLConnection httpsConn && Storage.getCertificateFile(bitcoindServer.getHost()) == null) {
|
||||
try {
|
||||
Certificate[] certs = httpsConn.getServerCertificates();
|
||||
if(certs.length > 0) {
|
||||
Storage.saveCertificate(bitcoindServer.getHost(), certs[0]);
|
||||
sslSocketFactory = null;
|
||||
}
|
||||
} catch(SSLPeerUnverifiedException e) {
|
||||
log.warn("Could not retrieve certificate for saving", e);
|
||||
}
|
||||
}
|
||||
|
||||
if(statusCode == 401) {
|
||||
throw new IOException((cookieFile == null ? "User/pass" : "Cookie file") + " authentication failed");
|
||||
}
|
||||
@@ -138,29 +154,24 @@ public class BitcoindTransport implements Transport {
|
||||
return bitcoindDir;
|
||||
}
|
||||
|
||||
private SSLSocketFactory getTrustAllSocketFactory() {
|
||||
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustAllCerts, null);
|
||||
return sslContext.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating SSL socket factory", e);
|
||||
private SSLSocketFactory getSSLSocketFactory() {
|
||||
if(sslSocketFactory == null) {
|
||||
sslSocketFactory = createSSLSocketFactory();
|
||||
}
|
||||
|
||||
return null;
|
||||
return sslSocketFactory;
|
||||
}
|
||||
|
||||
private SSLSocketFactory createSSLSocketFactory() {
|
||||
try {
|
||||
String host = bitcoindServer.getHost();
|
||||
TrustManager[] trustManagers = TcpOverTlsTransport.getTrustManagers(Storage.getCertificateFile(host), host);
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagers, null);
|
||||
return sslContext.getSocketFactory();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating SSL socket factory", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
package com.sparrowwallet.sparrow.net.cormorant.electrum;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -69,7 +71,7 @@ public class ElectrumServerRunnable implements Runnable {
|
||||
|
||||
private void openServerSocket() {
|
||||
try {
|
||||
serverSocket = new ServerSocket(0);
|
||||
serverSocket = new ServerSocket(0, 50, InetAddress.getByName(ElectrumServer.CORE_ELECTRUM_HOST));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException("Cannot open electrum server port", e);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java8.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -670,6 +670,9 @@ public class ServerSettingsController extends SettingsDetailController {
|
||||
if(exception.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File configCrtFile = Config.get().getElectrumServerCert();
|
||||
File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(savedCrtFile == null) {
|
||||
savedCrtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
|
||||
}
|
||||
if(configCrtFile == null && savedCrtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
|
||||
@@ -185,6 +185,9 @@ public class ServerTestDialog extends DialogWindow {
|
||||
if(exception.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File configCrtFile = Config.get().getElectrumServerCert();
|
||||
File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(savedCrtFile == null) {
|
||||
savedCrtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
|
||||
}
|
||||
if(configCrtFile == null && savedCrtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
|
||||
@@ -268,18 +268,28 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
return headersForm;
|
||||
}
|
||||
|
||||
private void setTransactionLocktime(Transaction tx, long locktime) {
|
||||
tx.setLocktime(locktime);
|
||||
if(headersForm.getPsbt() != null) {
|
||||
headersForm.getPsbt().setFallbackLocktime(locktime);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeView() {
|
||||
Transaction tx = headersForm.getTransaction();
|
||||
|
||||
updateTxId();
|
||||
|
||||
version.setValueFactory(new IntegerSpinner.ValueFactory(1, 2, (int)tx.getVersion()));
|
||||
version.setValueFactory(new IntegerSpinner.ValueFactory(1, 3, (int)tx.getVersion()));
|
||||
version.valueProperty().addListener((obs, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 1 || newValue > 2) {
|
||||
if(newValue == null || newValue < 1 || newValue > 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
tx.setVersion(newValue);
|
||||
if(headersForm.getPsbt() != null) {
|
||||
headersForm.getPsbt().setTxVersion((long)newValue);
|
||||
}
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
}
|
||||
@@ -296,7 +306,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
locktimeFieldset.getChildren().remove(locktimeBlockField);
|
||||
locktimeFieldset.getChildren().remove(locktimeNoneField);
|
||||
locktimeFieldset.getChildren().add(locktimeNoneField);
|
||||
tx.setLocktime(0);
|
||||
setTransactionLocktime(tx, 0);
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
}
|
||||
@@ -309,7 +319,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
if(block != null) {
|
||||
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && block < AppServices.getCurrentBlockHeight());
|
||||
futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && block > AppServices.getCurrentBlockHeight());
|
||||
tx.setLocktime(block);
|
||||
setTransactionLocktime(tx, block);
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
}
|
||||
@@ -323,7 +333,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
if(date != null) {
|
||||
locktimeDate.setDateTimeValue(date);
|
||||
futureDateWarning.setVisible(date.isAfter(LocalDateTime.now()));
|
||||
tx.setLocktime(date.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
|
||||
setTransactionLocktime(tx, date.atZone(ZoneId.systemDefault()).toEpochSecond());
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
}
|
||||
@@ -361,7 +371,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
return;
|
||||
}
|
||||
|
||||
tx.setLocktime(newValue);
|
||||
setTransactionLocktime(tx, newValue);
|
||||
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && newValue < AppServices.getCurrentBlockHeight());
|
||||
futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && newValue > AppServices.getCurrentBlockHeight());
|
||||
if(oldValue != null) {
|
||||
@@ -386,7 +396,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
int caret = locktimeDate.getEditor().getCaretPosition();
|
||||
locktimeDate.getEditor().setText(newValue.format(DateTimeFormatter.ofPattern(locktimeDate.getFormat())));
|
||||
locktimeDate.getEditor().positionCaret(caret);
|
||||
tx.setLocktime(newValue.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
|
||||
setTransactionLocktime(tx, newValue.atZone(ZoneId.systemDefault()).toEpochSecond());
|
||||
futureDateWarning.setVisible(newValue.isAfter(LocalDateTime.now()));
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
@@ -1441,7 +1451,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
public void transactionChanged(TransactionChangedEvent event) {
|
||||
if(headersForm.getTransaction().equals(event.getTransaction())) {
|
||||
updateTxId();
|
||||
boolean locktimeEnabled = headersForm.getTransaction().isLocktimeSequenceEnabled();
|
||||
boolean locktimeEnabled = headersForm.isEditable() && headersForm.getTransaction().isLocktimeSequenceEnabled();
|
||||
locktimeNoneType.setDisable(!locktimeEnabled);
|
||||
locktimeBlockType.setDisable(!locktimeEnabled);
|
||||
locktimeBlock.setDisable(!locktimeEnabled);
|
||||
|
||||
@@ -330,14 +330,14 @@ public class InputController extends TransactionFormController implements Initia
|
||||
if(txInput.isAbsoluteTimeLockDisabled()) {
|
||||
locktimeToggleGroup.selectToggle(locktimeAbsoluteType);
|
||||
} else if(txInput.isAbsoluteTimeLocked()) {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
|
||||
setInputSequenceNumber(txInput, TransactionInput.SEQUENCE_RBF_ENABLED);
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(txInput.isAbsoluteTimeLocked()) {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
setInputSequenceNumber(txInput, TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
}
|
||||
@@ -366,6 +366,13 @@ public class InputController extends TransactionFormController implements Initia
|
||||
}
|
||||
}
|
||||
|
||||
private void setInputSequenceNumber(TransactionInput txInput, long sequence) {
|
||||
txInput.setSequenceNumber(sequence);
|
||||
if(inputForm.getPsbtInput() != null) {
|
||||
inputForm.getPsbtInput().setSequence(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeLocktimeFields(TransactionInput txInput) {
|
||||
Transaction transaction = inputForm.getTransaction();
|
||||
locktimeToggleGroup.selectedToggleProperty().addListener((ov, old_toggle, new_toggle) -> {
|
||||
@@ -376,7 +383,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||
locktimeFieldset.getChildren().add(locktimeAbsoluteField);
|
||||
updateAbsoluteLocktimeField(transaction);
|
||||
locktimeAbsoluteField.setDisable(true);
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_LOCKTIME_DISABLED);
|
||||
setInputSequenceNumber(txInput, TransactionInput.SEQUENCE_LOCKTIME_DISABLED);
|
||||
rbf.setSelected(false);
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
@@ -387,9 +394,9 @@ public class InputController extends TransactionFormController implements Initia
|
||||
updateAbsoluteLocktimeField(transaction);
|
||||
locktimeAbsoluteField.setDisable(false);
|
||||
if(rbf.selectedProperty().getValue()) {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_ENABLED);
|
||||
setInputSequenceNumber(txInput, TransactionInput.SEQUENCE_RBF_ENABLED);
|
||||
} else {
|
||||
txInput.setSequenceNumber(TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
setInputSequenceNumber(txInput, TransactionInput.SEQUENCE_RBF_DISABLED);
|
||||
}
|
||||
if(old_toggle != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
@@ -468,10 +475,10 @@ public class InputController extends TransactionFormController implements Initia
|
||||
String relativeSelection = locktimeRelativeCombo.getValue();
|
||||
if(relativeSelection.equals("blocks")) {
|
||||
Integer value = locktimeRelativeBlocks.getValue();
|
||||
txInput.setSequenceNumber(value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK);
|
||||
setInputSequenceNumber(txInput, value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK);
|
||||
} else {
|
||||
long value = locktimeRelativeSeconds.getValue().toSeconds() / TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT;
|
||||
txInput.setSequenceNumber((value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) | TransactionInput.RELATIVE_TIMELOCK_TYPE_FLAG);
|
||||
setInputSequenceNumber(txInput, (value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) | TransactionInput.RELATIVE_TIMELOCK_TYPE_FLAG);
|
||||
}
|
||||
if(changed) {
|
||||
EventManager.get().post(new TransactionChangedEvent(transaction));
|
||||
|
||||
@@ -44,7 +44,6 @@ open module com.sparrowwallet.sparrow {
|
||||
requires io.reactivex.rxjava2fx;
|
||||
requires org.apache.commons.lang3;
|
||||
requires org.apache.commons.compress;
|
||||
requires net.sourceforge.streamsupport;
|
||||
requires co.nstant.in.cbor;
|
||||
requires com.github.librepdf.openpdf;
|
||||
requires com.googlecode.lanterna;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
-fx-base: -fx-default-button;
|
||||
}
|
||||
|
||||
.split-menu-button.default-button > .label, .menu-item.default-button > .label {
|
||||
.split-menu-button > .label, .menu-item.default-button > .label {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class DescriptorTest extends IoTest {
|
||||
@Test
|
||||
public void testImport() throws ImportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive.txt"), null);
|
||||
|
||||
Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(keystore.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportMultipath() throws ImportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
|
||||
|
||||
Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(keystore.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportSeparateDescriptors() throws ImportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive-change1.txt"), null);
|
||||
|
||||
Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(keystore.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExport() throws ImportException, ExportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
descriptor.exportWallet(wallet, baos, null);
|
||||
String export = baos.toString();
|
||||
|
||||
Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/<0;1>/*)#cpx4ean7"));
|
||||
Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda"));
|
||||
Assertions.assertTrue(export.contains("wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportExport() throws ImportException, ExportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-multipath.txt"), null);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
descriptor.exportWallet(wallet, baos, null);
|
||||
|
||||
Wallet reimported = descriptor.importWallet(new ByteArrayInputStream(baos.toByteArray()), null);
|
||||
|
||||
Assertions.assertEquals(wallet.getScriptType(), reimported.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Keystore reimportedKeystore = reimported.getKeystores().getFirst();
|
||||
Assertions.assertEquals(keystore.getKeyDerivation().getDerivationPath(), reimportedKeystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals(keystore.getKeyDerivation().getMasterFingerprint(), reimportedKeystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(keystore.getExtendedPublicKey(), reimportedKeystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(reimportedKeystore.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportLabelled() throws ImportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-labelled.txt"), null);
|
||||
|
||||
Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(keystore.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImportSeparateDescriptorsNoBlankLine() throws ImportException {
|
||||
Descriptor descriptor = new Descriptor();
|
||||
Wallet wallet = descriptor.importWallet(getInputStream("descriptor-receive-change2.txt"), null);
|
||||
|
||||
Assertions.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
Assertions.assertEquals("m/84'/0'/0'", keystore.getKeyDerivation().getDerivationPath());
|
||||
Assertions.assertEquals("a262308d", keystore.getKeyDerivation().getMasterFingerprint());
|
||||
Assertions.assertEquals(ExtendedKey.fromDescriptor("xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz"), keystore.getExtendedPublicKey());
|
||||
Assertions.assertTrue(keystore.isValid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Receive: wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
|
||||
@@ -0,0 +1 @@
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/<0;1>/*)#cpx4ean7
|
||||
@@ -0,0 +1,3 @@
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
|
||||
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9
|
||||
@@ -0,0 +1,2 @@
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/1/*)#pqrw8ra9
|
||||
@@ -0,0 +1 @@
|
||||
wpkh([a262308d/84h/0h/0h]xpub6DM7CYgaTMdMbhTcLTUWmNUE5WLXK5hx8ZMa4sRw8qYJPqtqKYiKnwsmT8A6AijDVAUZRivdBnXdR8QE7Y9vVnqvzPL3fXCmu1WtCRLdAoz/0/*)#s5x06kda
|
||||
Reference in New Issue
Block a user