Compare commits

..
38 Commits
Author SHA1 Message Date
Craig Raw 67e1573329 improve reset instructions for trezor passphrase change 2026-03-09 13:44:32 +02:00
Craig Raw 23a9db5fc6 support loading v3 transactions in the transaction editor 2026-03-06 14:35:42 +02:00
Craig Raw e9108b85cd fix possible db corruption on password removal by reordering database file and row updates 2026-03-06 11:30:41 +02:00
Craig Raw 3766585474 fix psbtv2 and dst related transaction editor issues around tx version and locktime 2026-03-06 09:15:57 +02:00
Craig Raw 0e7aa34d07 fix non default split menu button text color on dark theme 2026-03-05 13:46:32 +02:00
Craig Raw 537c2ffef0 support qr and file methods for signing messages via psbt when bip322 is selected 2026-03-05 13:37:40 +02:00
Craig Raw 8b4f388eb7 add tests for descriptor import and export, and handle multiline descriptor 2026-03-05 09:39:33 +02:00
Craig Raw 6a07adfea5 upgrade jackson-databind to v2.21.1 2026-03-04 15:14:59 +02:00
Craig Raw eeade6a173 upgrade gson to v2.13.2 2026-03-04 14:36:29 +02:00
Craig Raw 4c2b8055ec upgrade protobuf to v4.34.0 2026-03-04 14:15:28 +02:00
Craig Raw 04d210ed4b upgrade bcprov to v1.82 and pgpainless to v1.7.7 2026-03-04 14:07:01 +02:00
Craig Raw 5b737be76d upgrade controlsfx to v11.2.3 2026-03-04 13:50:06 +02:00
Craig Raw 908fb80d2a upgrade kotlinx-coroutines-javafx to v1.10.2 2026-03-04 13:37:53 +02:00
Craig Raw eda7087bdd upgrade junit to v5.14.1 2026-03-04 13:05:19 +02:00
Craig Raw a5d61f1a9f upgrade jetbrains annotations to v26.0.2 2026-03-04 13:02:21 +02:00
Craig Raw 2621bb6504 upgrade thumbnailator to v0.4.21 2026-03-04 12:59:33 +02:00
Craig Raw 7dfc432bd2 remove streamsupport dependency 2026-03-04 12:54:16 +02:00
Craig Raw df5b5146ba upgrade jcommander to v3.0 2026-03-04 12:46:10 +02:00
Craig Raw 452584d87d upgrade zxing to v3.5.4 2026-03-04 12:37:24 +02:00
Craig Raw 5c823533d3 upgrade caffeine to v3.2.3 2026-03-04 12:34:52 +02:00
Craig Raw 6561971d4f upgrade dnsjava to v3.6.4 2026-03-04 12:32:05 +02:00
Craig Raw 8ebf8dc041 upgrade rxjava2 to v2.2.21 2026-03-04 12:21:29 +02:00
Craig Raw 419e8e1374 upgrade argon2-jvm to v2.12 2026-03-04 12:14:01 +02:00
Craig Raw 684965cf89 upgrade commons-codec to v1.21.0 2026-03-04 12:09:43 +02:00
Craig Raw 3498b5bab1 upgrade commons-compress to v1.28.0 2026-03-04 12:06:46 +02:00
Craig Raw 74635b1d1f upgrade commons-lang3 to v3.20.0 2026-03-04 12:02:25 +02:00
Craig Raw 229c6430ed upgrade jdbi to v3.51.0 2026-03-04 12:00:14 +02:00
Craig Raw c74abfeeb7 upgrade hikaricp to v7.0.2 2026-03-04 11:55:59 +02:00
Craig Raw c78bc7b1cf upgrade openpdf to v1.3.43 2026-03-04 11:49:54 +02:00
Craig Raw 87cd96a627 upgrade jna to v5.18.1 2026-03-04 11:36:08 +02:00
Craig Raw 7ae04a2564 upgrade slf4j-api and jul-to-slf4j to v2.0.17 2026-03-04 11:30:59 +02:00
Craig Raw d4c0df0df9 detect algorithm on provided certificate when checking for ca cert 2026-02-28 12:51:50 +02:00
Craig Raw dc88dc4ede add hostname verification for ca-validated tls connections 2026-02-28 12:38:58 +02:00
Craig Raw a478edfad7 use ca validation or tofu pinning for public servers depending on certificate type 2026-02-27 13:30:46 +02:00
Craig Raw 46d444615c implement tofu certificate pinning for tls bitcoin core connections 2026-02-19 15:35:03 +02:00
Craig Raw 78fe55787b escape backticks in schema name for drop schema statement 2026-02-18 16:16:37 +02:00
Craig Raw cd0be365de bind cormorant server to localhost only 2026-02-18 15:45:51 +02:00
Craig Raw b9d62c0b7b bump to v2.4.2 2026-02-17 10:41:24 +02:00
32 changed files with 516 additions and 130 deletions
+19 -30
View File
@@ -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')
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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());
@@ -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;
}
}
}
@@ -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));
-1
View File
@@ -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