Compare commits

...
91 Commits
Author SHA1 Message Date
Craig Raw 9e4eed965c show file name in invalid file dialog 2024-03-06 14:17:15 +02:00
Craig Raw c034dbe89e better handling of multiple verification file drop 2024-03-06 14:11:37 +02:00
Craig Raw 4cb2e1ef9f show bbqr option for bip129 and file menu qr transaction display 2024-03-06 13:33:13 +02:00
Craig Raw 7258b049c9 followup 2024-03-06 12:40:57 +02:00
Craig Raw 5475a81e3a avoid disabling public key field when user provided key is used 2024-03-06 12:31:08 +02:00
Craig Raw f003b6d732 improve asc file type description 2024-03-06 08:40:52 +02:00
Craig Raw c34a423f95 followup 2024-03-05 15:10:33 +02:00
Craig Raw 55e7689f7c open files and uris after initial wallet loading 2024-03-05 14:09:52 +02:00
Craig Raw 914afe9a8d perform gpg verification in separate thread 2024-03-05 12:52:37 +02:00
Craig Raw 81c7bc7ecb improvements to download verifier drag and drop 2024-03-05 12:23:11 +02:00
Craig Raw 3d18477560 add application/pgp-signature to handled mime types 2024-03-04 16:14:46 +02:00
Craig Raw 2c1f591c51 handle drop of manifest files as well 2024-03-04 15:30:25 +02:00
Craig Raw d109eaa654 handle signature and manifest file mixups, add file handler for .asc files 2024-03-04 15:07:56 +02:00
Craig Raw 803e43cb45 if validating derivations, disallow paths that match other networks 2024-03-01 11:24:21 +02:00
Craig Raw a45024ac70 followup 2024-03-01 10:06:30 +02:00
Craig Raw 0f05502af6 allow adding additional accounts (up to account 30) if accounts 0-9 have already been added 2024-03-01 09:51:23 +02:00
Craig Raw 6a496894e1 add import file to krux keystore and wallet import 2024-03-01 07:50:00 +02:00
Craig Raw d3b0eac51a improve qr reading by additionally scanning using boofcv 2024-02-29 15:32:29 +02:00
Craig Raw 2cc02e38e6 add restart in signet to tools menu, add mempool.space signet public server 2024-02-28 14:52:52 +02:00
Craig Raw ae29108656 cormorant: fix wallet loading with multiple ancestors in the mempool 2024-02-28 13:13:32 +02:00
Craig Raw 0ed8c6af7c improve script area display of inputs that spend a taproot script path 2024-02-28 10:29:14 +02:00
Craig Raw 6d7f02227a add seconds to date column for transactions and utxos csv export 2024-02-27 15:35:26 +02:00
Craig Raw 8f52039c7b followup 2024-02-27 15:27:26 +02:00
craigrawandGitHub f14e2fb020 Merge pull request #1281 from PrinceOfEgypt/master
add delay to rates service startup
2024-02-27 15:23:45 +02:00
PrinceOfEgypt cc9a557a2e Configure startup delay by platform 2024-02-26 09:22:17 -06:00
Craig Raw e50cc7126d make coldcard import and export functions scannable 2024-02-26 16:43:56 +02:00
PrinceOfEgyptandGitHub 6cdbba4bb3 Merge branch 'sparrowwallet:master' into master 2024-02-26 08:41:54 -06:00
Craig Raw 7f3885611a add support for bbqr encoding and decoding 2024-02-26 11:41:10 +02:00
PrinceOfEgypt 17ea75603f Create separate constant for RatesService delay 2024-02-25 12:14:11 -06:00
PrinceOfEgypt da1626070b Add 2 second delay to RatesService 2024-02-24 09:36:02 -06:00
Craig Raw 6f4d37d3ff confirm and close application before launching installer 2024-02-22 14:56:38 +02:00
Craig Raw 78b0c63f87 test for windows gpg home 2024-02-22 14:12:24 +02:00
Craig Raw 14d96bdb4e match gpg behaviour for loading user public keyrings 2024-02-22 13:50:58 +02:00
Craig Raw d73820464e add download verification dialog supporting pgp signatures and optional sha256 manifests 2024-02-22 13:35:06 +02:00
Craig Raw ff4ff90bb2 revert to java 18 due to windows code signing issue 2024-02-16 11:27:24 +02:00
Craig Raw d2940265fd fix drongo typo 2024-02-16 08:11:24 +02:00
Craig Raw 3f72a84afe increase whirlpool http client timeout 2024-02-16 07:58:46 +02:00
Craig Raw 521bbdd70e revert to build on ubuntu 20.04 for xz compression 2024-02-15 16:50:05 +02:00
Craig Raw 15c3150a4f update to build on ubuntu 22.0.4 2024-02-15 13:09:22 +02:00
Craig Raw 865c52e5d9 followup 2024-02-15 11:39:46 +02:00
Craig Raw ee195f2677 upgrade build to java 21 2024-02-15 10:32:29 +02:00
Craig Raw 1d50b4f296 upgrade junit tests from 4 to 5 2024-02-15 10:26:29 +02:00
Craig Raw 22310cd8c9 update guava, gson, junit, nightjar and slf4j dependencies 2024-02-15 09:24:21 +02:00
Craig Raw 78406fd024 include labels for hidden addresses in address csv export 2024-02-07 09:26:26 +02:00
Craig Raw 74a551ed01 add support for satschip nfc card 2024-02-07 09:19:55 +02:00
craigrawandGitHub 4de6bd5e83 Merge pull request #1256 from rex4539/typos
fix typos
2024-02-05 07:42:21 +02:00
Dimitris Apostolou 8d6230e834 Fix typos 2024-02-02 23:11:36 +02:00
Craig Raw 31042039d7 upgrade to hwi 2.4.0 2024-02-01 10:45:19 +02:00
Craig Raw 1ba501f5c8 add trezor safe 3 support (hwi update still required) 2024-01-30 09:37:00 +02:00
Craig Raw 55d5a97d99 avoid saving certificates for public servers 2024-01-30 08:03:38 +02:00
Craig Raw 37af663511 update gradle wrapper to 8.5 2024-01-29 14:35:58 +02:00
Craig Raw b757c76028 update drongo build with java 21 2024-01-29 12:21:29 +02:00
craigrawandGitHub 82e0b63ed0 Merge pull request #1244 from msgilligan/msgilligan-gradle-jlink-3.0.1
update jlink plugin to build with java 21
2024-01-29 12:20:21 +02:00
Sean Gilligan 7da3a55489 build.gradle: Update to JLink plugin 3.0.1
This adds support for building with JDK 21.
2024-01-24 10:27:25 -08:00
Craig Raw 7ebb92d90a crop y axis range in block target fee rates chart 2024-01-24 09:21:04 +02:00
Craig Raw 1d32b69345 add border to expanded transaction diagram on linux to handle some window managers 2024-01-22 09:52:32 +02:00
Craig Raw fbc49fd6f5 change cancel button text to close on qr display and scan dialogs 2024-01-22 09:15:55 +02:00
Craig Raw 30001051c7 avoid null labels when broadcasting a loaded transaction with no name 2024-01-22 09:12:00 +02:00
Craig Raw 1e74ae5f19 bump to v1.8.3 2024-01-18 13:01:59 +02:00
Craig Raw 6fc52fdc0e display effective fee rate next to transaction fee rate when constructing a cpfp tx 2024-01-18 09:53:44 +02:00
Craig Raw 1d2081d2a6 transaction tree label tweaks 2024-01-18 09:00:39 +02:00
Craig Raw 4d587cf776 update keystone logo 2024-01-18 08:35:01 +02:00
Craig Raw 14689dd256 add payment labels to transaction tabs for received outputs where available from open wallets 2024-01-17 14:43:24 +02:00
Craig Raw c8a5c1fb86 followup 2024-01-17 13:50:04 +02:00
Craig Raw 734818cd9e adjust transaction tree width for input labelling 2024-01-17 13:43:48 +02:00
Craig Raw 6aa5473b24 add rescan note to warning tooltip for large gap limits 2024-01-17 12:33:55 +02:00
Craig Raw 1b460533f5 followup to reducing server calls on loading transaction tab 2024-01-17 10:48:57 +02:00
Craig Raw ad1ecfb887 fix changing amount bitcoin unit on send tab with comma decimal separator 2024-01-16 12:19:56 +02:00
Craig Raw 6a5060c0c8 cormorant: support coinbase transactions 2024-01-15 17:20:44 +02:00
Craig Raw 02a0a3277b reduce server calls on opening a transaction tab by using open wallet history when performing spent output lookup 2024-01-12 15:35:26 +02:00
Craig Raw 57dba5d6ae improve input and output labels on transaction tree and detail panels 2024-01-12 12:10:38 +02:00
Craig Raw 540424a2e3 followup 2024-01-10 11:18:26 +02:00
craigrawandGitHub e1d9d54da3 Merge pull request #1204 from krzyczak/master
toggle bitcoin unit on clicking a bitcoin value label
2024-01-10 11:15:47 +02:00
Craig Raw 2b8a513adf support reading revised ur tags including v3 output descriptors 2024-01-10 11:05:03 +02:00
Craig Raw 20df1dbd8b fix typo 2024-01-09 11:59:19 +02:00
Craig Raw 04c8017bb5 increase payments tab header width on send tab 2024-01-09 11:58:38 +02:00
Craig Raw be0ac52a09 support creating wallets from descriptors containing master xprvs 2024-01-09 11:38:10 +02:00
Craig Raw 3162371838 fix order of outputs displayed in transaction diagram where there are multiple payments with the same address and amount 2024-01-08 12:12:13 +02:00
Craig Raw b5196d1ac2 encrypt electrum wallet exports including private keys where password is available 2024-01-08 09:16:55 +02:00
krzyczak 343cb2271c feat: Implement bitcoin unit toggle on bitcoin balance 2023-12-22 17:29:22 +00:00
Craig Raw 4feb4a3a79 add airgap vault to airgapped options (draft) 2023-12-06 08:55:09 +02:00
craigrawandGitHub 20b4d5a1b5 Merge pull request #1187 from Toporin/patch-satochip-error-0x9C01
satochip: flush card cache where cache memory is full
2023-12-01 09:12:58 +02:00
Craig Raw 06489d8339 cormorant: round up wallet range to avoid frequent rescans with a large gap limit 2023-12-01 09:09:28 +02:00
Toporin e368782b4c Satochip: Patch error 0x9C01 in cardBip32GetExtendedKey()
This error occurs when the cache memory in the card is full.
We need to flush the cache by sending the same command with the p2 flag set to 0x80.
2023-11-30 21:25:01 +01:00
Craig Raw 6ee3755ce4 cormorant: fix descriptor range calculation and extend range from pruned date only where necessary 2023-11-30 12:54:50 +02:00
Craig Raw d425933189 only allow sweeping from uncompressed keys to legacy script type addresses 2023-11-30 11:42:59 +02:00
Craig Raw 675b7ed4f8 cormorant: default to existing descriptor timestamp when extending range 2023-11-30 08:50:20 +02:00
Craig Raw 6072f6d31a add master fingerprint to passphrase entry dialog in terminal 2023-11-29 10:25:40 +02:00
Craig Raw e1fb674170 avoid showing balance in fiat when exchange source is none 2023-11-29 09:32:01 +02:00
Craig Raw d8839763a4 remove extraneous space character 2023-11-23 07:49:20 +02:00
Craig Raw 64e98cdb35 bump to v1.8.2 2023-11-22 11:59:02 +02:00
153 changed files with 2983 additions and 1423 deletions
+4 -4
View File
@@ -12,11 +12,11 @@ jobs:
matrix:
os: [windows-2022, ubuntu-20.04, macos-12]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Set up JDK 18.0.1
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '18.0.1'
@@ -31,7 +31,7 @@ jobs:
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Upload Artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }}
path: |
@@ -48,7 +48,7 @@ jobs:
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
- name: Upload Headless Artifact
if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Sparrow Build - ${{ runner.os }} Headless
path: |
+50 -60
View File
@@ -2,12 +2,12 @@ import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'org-openjfx-javafxplugin'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.26.0'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'org.beryx.jlink' version '3.0.1'
}
def sparrowVersion = '1.8.1'
def sparrowVersion = '1.8.3'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -63,18 +63,19 @@ java {
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo')) {
exclude group: 'org.hamcrest'
exclude group: 'junit'
}
implementation('com.google.guava:guava:31.1-jre')
implementation('com.google.code.gson:gson:2.8.6')
implementation(project(':drongo'))
implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:4.0.3')
implementation('com.zaxxer:HikariCP:4.0.3') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-core:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
@@ -89,7 +90,7 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.7')
implementation('com.sparrowwallet:hummingbird:1.7.3')
implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
@@ -118,25 +119,26 @@ dependencies {
}
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
implementation ('org.slf4j:slf4j-api:2.0.12')
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.39')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.40')
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('org.apache.commons:commons-compress:1.25.0')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30')
implementation('com.googlecode.lanterna:lanterna:3.1.1')
implementation('net.coobird:thumbnailator:0.4.18')
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
implementation('com.sparrowwallet:toucan:0.9.0')
testImplementation('junit:junit:4.13.1')
}
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
implementation('com.jcraft:jzlib:1.1.3')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
compileJava {
@@ -157,10 +159,14 @@ processResources {
}
test {
jvmArgs '--add-opens=java.base/java.io=com.google.gson'
useJUnitPlatform()
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
}
run {
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
@@ -236,7 +242,8 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
"--add-reads=com.sparrowwallet.merged.module=ch.qos.logback.classic",
"--add-reads=com.sparrowwallet.merged.module=org.slf4j",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
@@ -260,7 +267,7 @@ jlink {
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
@@ -405,41 +412,16 @@ extraJavaModuleInfo {
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
exports('com.csvreader')
}
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
exports('org.slf4j.bridge')
requires('java.logging')
requires('org.slf4j')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
}
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
exports('org.json.simple.parser')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('guava-31.1-jre.jar', 'com.google.common', '31.1-jre') {
exports('com.google.common.eventbus')
exports('com.google.common.net')
exports('com.google.common.base')
exports('com.google.common.collect')
exports('com.google.common.io')
exports('com.google.common.primitives')
exports('com.google.common.math')
requires('failureaccess')
requires('java.logging')
}
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
@@ -450,6 +432,7 @@ extraJavaModuleInfo {
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
requires('com.github.benmanes.caffeine')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
@@ -508,7 +491,7 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('nightjar-0.2.39.jar', 'com.sparrowwallet.nightjar', '0.2.39') {
module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
@@ -516,7 +499,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('ch.qos.logback.classic')
requires('org.json')
requires('io.reactivex.rxjava2')
exports('com.samourai.http.client')
@@ -719,15 +702,22 @@ extraJavaModuleInfo {
exports('io.matthewnelson.component.parcelize')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
requires('java.xml')
}
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
requires('org.hamcrest.core')
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
exports('org.pgpainless')
exports('org.pgpainless.key')
exports('org.pgpainless.key.parsing')
exports('org.pgpainless.decryption_verification')
exports('org.pgpainless.exception')
exports('org.pgpainless.signature')
exports('org.pgpainless.util')
requires('org.bouncycastle.provider')
requires('org.bouncycastle.pg')
requires('org.slf4j')
}
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
exports('com.jcraft.jzlib')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
+1 -7
View File
@@ -3,9 +3,7 @@ plugins {
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
implementation 'org.javamodularity:moduleplugin:1.8.12'
implementation 'org.ow2.asm:asm:9.6'
}
repositories {
@@ -22,9 +20,5 @@ gradlePlugin {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
register("org-openjfx-javafxplugin") {
id = "org-openjfx-javafxplugin"
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
}
}
}
@@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
@@ -1,114 +0,0 @@
/*
* Copyright (c) 2018, 2020, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import org.gradle.api.GradleException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public enum JavaFXModule {
BASE,
GRAPHICS(BASE),
CONTROLS(BASE, GRAPHICS),
FXML(BASE, GRAPHICS),
MEDIA(BASE, GRAPHICS),
SWING(BASE, GRAPHICS),
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
static final String PREFIX_MODULE = "javafx.";
private static final String PREFIX_ARTIFACT = "javafx-";
private List<JavaFXModule> dependentModules;
JavaFXModule(JavaFXModule...dependentModules) {
this.dependentModules = List.of(dependentModules);
}
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
return Stream.of(JavaFXModule.values())
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
.findFirst();
}
public String getModuleName() {
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
}
public String getModuleJarFileName() {
return getModuleName() + ".jar";
}
public String getArtifactName() {
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
}
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
return p.matcher(jarFileName).matches();
}
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
validateModules(moduleNames);
return moduleNames.stream()
.map(JavaFXModule::fromModuleName)
.flatMap(Optional::stream)
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
.collect(Collectors.toSet());
}
public static void validateModules(List<String> moduleNames) {
var invalidModules = moduleNames.stream()
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
.collect(Collectors.toList());
if (! invalidModules.isEmpty()) {
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
}
}
public List<JavaFXModule> getDependentModules() {
return dependentModules;
}
public List<JavaFXModule> getMavenDependencies() {
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
dependencies.add(0, this);
return dependencies;
}
}
@@ -1,164 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import org.gradle.api.Project;
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
public class JavaFXOptions {
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
private final Project project;
private final JavaFXPlatform platform;
private String version = "16";
private String sdk;
private String configuration = "implementation";
private String lastUpdatedConfiguration;
private List<String> modules = new ArrayList<>();
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
public JavaFXOptions(Project project) {
this.project = project;
this.platform = JavaFXPlatform.detect(project);
}
public JavaFXPlatform getPlatform() {
return platform;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
updateJavaFXDependencies();
}
/**
* If set, the JavaFX modules will be taken from this local
* repository, and not from Maven Central
* @param sdk, the path to the local JavaFX SDK folder
*/
public void setSdk(String sdk) {
this.sdk = sdk;
updateJavaFXDependencies();
}
public String getSdk() {
return sdk;
}
/** Set the configuration name for dependencies, e.g.
* 'implementation', 'compileOnly' etc.
* @param configuration The configuration name for dependencies
*/
public void setConfiguration(String configuration) {
this.configuration = configuration;
updateJavaFXDependencies();
}
public String getConfiguration() {
return configuration;
}
public List<String> getModules() {
return modules;
}
public void setModules(List<String> modules) {
this.modules = modules;
updateJavaFXDependencies();
}
public void modules(String...moduleNames) {
setModules(List.of(moduleNames));
}
private void updateJavaFXDependencies() {
clearJavaFXDependencies();
String configuration = getConfiguration();
JavaFXModule.getJavaFXModules(this.modules).stream()
.sorted()
.forEach(javaFXModule -> {
if (customSDKArtifactRepository != null) {
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
} else {
project.getDependencies().add(configuration,
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
getVersion(), getPlatform().getClassifier()));
}
});
lastUpdatedConfiguration = configuration;
}
private void clearJavaFXDependencies() {
if (customSDKArtifactRepository != null) {
project.getRepositories().remove(customSDKArtifactRepository);
customSDKArtifactRepository = null;
}
if (sdk != null && ! sdk.isEmpty()) {
Map<String, String> dirs = new HashMap<>();
dirs.put("name", "customSDKArtifactRepository");
if (sdk.endsWith(File.separator)) {
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
} else {
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
}
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
}
if (lastUpdatedConfiguration == null) {
return;
}
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
if (configuration != null) {
if (customSDKArtifactRepository != null) {
configuration.getDependencies()
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
}
configuration.getDependencies()
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetector;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import java.awt.*;
import java.util.Arrays;
import java.util.stream.Collectors;
public enum JavaFXPlatform {
LINUX("linux", "linux-x86_64"),
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
WINDOWS("win", "windows-x86_64"),
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
OSX("mac", "osx-x86_64"),
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
private final String classifier;
private final String osDetectorClassifier;
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
this.classifier = classifier;
this.osDetectorClassifier = osDetectorClassifier;
}
public String getClassifier() {
return classifier;
}
public static JavaFXPlatform detect(Project project) {
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
if("true".equals(System.getProperty("java.awt.headless"))) {
osClassifier += "-monocle";
}
for ( JavaFXPlatform platform: values()) {
if ( platform.osDetectorClassifier.equals(osClassifier)) {
return platform;
}
}
String supportedPlatforms = Arrays.stream(values())
.map(p->p.osDetectorClassifier)
.collect(Collectors.joining("', '", "'", "'"));
throw new GradleException(
String.format(
"Unsupported JavaFX platform found: '%s'! " +
"This plugin is designed to work on supported platforms only." +
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
);
}
}
@@ -1,49 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetectorPlugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().create("configJavafxRun", ExecTask.class, project);
}
}
@@ -1,124 +0,0 @@
/*
* Copyright (c) 2019, 2021, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle.tasks;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
import org.openjfx.gradle.JavaFXModule;
import org.openjfx.gradle.JavaFXOptions;
import org.openjfx.gradle.JavaFXPlatform;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
private final Project project;
private JavaExec execTask;
@Inject
public ExecTask(Project project) {
this.project = project;
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
if (execTask != null) {
execTask.dependsOn(this);
} else {
throw new GradleException("Run task not found.");
}
});
}
@TaskAction
public void action() {
if (execTask != null) {
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
JavaFXModule.validateModules(javaFXOptions.getModules());
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
if (!definedJavaFXModuleNames.isEmpty()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
);
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
if (moduleOptions != null) {
LOGGER.info("Modular JavaFX application found");
// Remove empty JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
} else {
LOGGER.info("Non-modular JavaFX application found");
// Remove all JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars);
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
var jvmArgs = new ArrayList<String>();
jvmArgs.add("--add-modules");
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
List<String> execJvmArgs = execTask.getJvmArgs();
if (execJvmArgs != null) {
jvmArgs.addAll(execJvmArgs);
}
jvmArgs.addAll(javaFXModuleJvmArgs);
execTask.setJvmArgs(jvmArgs);
}
}
} else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
}
}
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
return jar.isFile() &&
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
javaFXModule.compareJarFileName(platform, jar.getName()) ||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
}
}
+2 -2
View File
@@ -42,7 +42,7 @@ Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
```
gpg --import --import-options show-only adoptium.asc
```
If key doesn't match, do not procede.
If key doesn't match, do not proceed.
Add Adoptium PGP key to a the keyring shared folder:
```sh
@@ -82,7 +82,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="1.8.0"
GIT_TAG="1.8.2"
```
The project can then be initially cloned as follows:
+1 -1
Submodule drongo updated: 0bb5b75be5...6868b026fb
Binary file not shown.
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+17 -12
View File
@@ -83,10 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,10 +131,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
+3
View File
@@ -0,0 +1,3 @@
mime-type=application/pgp-signature
extension=asc
description=ASCII Armored File
@@ -6,5 +6,5 @@ Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Finance;Network;
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
StartupWMClass=Sparrow
+16 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.8.1</string>
<string>1.8.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -94,6 +94,21 @@
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>com.sparrowwallet.asc</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>asc</string>
</array>
</dict>
<key>UTTypeDescription</key>
<string>ASCII Armored File</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
@@ -21,6 +21,8 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
@@ -77,6 +79,7 @@ import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(AppController.class);
@@ -198,7 +201,7 @@ public class AppController implements Initializable {
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@FXML
private MenuItem restart;
private Menu restart;
@FXML
private StackPane rootStack;
@@ -218,6 +221,8 @@ public class AppController implements Initializable {
private SendToManyDialog sendToManyDialog;
private DownloadVerifierDialog downloadVerifierDialog;
private Tab previouslySelectedTab;
private boolean subTabsVisible;
@@ -277,7 +282,7 @@ public class AppController implements Initializable {
rootStack.setOnDragOver(event -> {
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.acceptTransferModes(TransferMode.LINK);
}
event.consume();
});
@@ -286,9 +291,7 @@ public class AppController implements Initializable {
Dragboard db = event.getDragboard();
boolean success = false;
if(db.hasFiles()) {
for(File file : db.getFiles()) {
openFile(file);
}
openFiles(db.getFiles());
success = true;
}
event.setDropCompleted(success);
@@ -375,7 +378,14 @@ public class AppController implements Initializable {
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
restart.setText("Restart in " + (Network.get() == Network.MAINNET ? Network.TESTNET.toDisplayString() : Network.MAINNET.toDisplayString()));
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
networks.remove(Network.get());
for(Network network : networks) {
MenuItem networkItem = new MenuItem(network.toDisplayString());
networkItem.setOnAction(event -> restart(event, network));
restart.getItems().add(networkItem);
}
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
saveTransaction.setDisable(true);
@@ -626,7 +636,7 @@ public class AppController implements Initializable {
} catch(TransactionParseException e) {
showErrorDialog("Invalid transaction", e.getMessage());
} catch(Exception e) {
showErrorDialog("Invalid file", e.getMessage());
showErrorDialog("Invalid file", "Cannot recognise the format of the " + file.getName() + " file.");
}
}
}
@@ -742,8 +752,10 @@ public class AppController implements Initializable {
Transaction transaction = transactionTabData.getTransaction();
try {
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
byte[] txBytes = transaction.bitcoinSerialize();
UR ur = UR.fromBytes(txBytes);
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
qrDisplayDialog.showAndWait();
} catch(Exception e) {
@@ -841,8 +853,10 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
qrDisplayDialog.show();
}
@@ -957,7 +971,7 @@ public class AppController implements Initializable {
AppServices.get().setPreventSleep(item.isSelected());
}
public void restart(ActionEvent event) {
public void restart(ActionEvent event, Network network) {
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
}
@@ -968,7 +982,7 @@ public class AppController implements Initializable {
jCommander.parse(argv);
});
args.network = (Network.get() == Network.MAINNET ? Network.TESTNET : Network.MAINNET);
args.network = network;
try {
List<String> cmd = new ArrayList<>();
@@ -982,11 +996,19 @@ public class AppController implements Initializable {
}
}
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
} else {
openTransactionFile(file);
public void openFiles(List<File> files) {
boolean verifyOpened = false;
for(File file : files) {
if(isWalletFile(file)) {
openWalletFile(file, true);
} else if(isVerifyDownloadFile(file)) {
if(!verifyOpened) {
verifyDownload(new ActionEvent(file, rootStack));
verifyOpened = true;
}
} else {
openTransactionFile(file);
}
}
}
@@ -1468,6 +1490,23 @@ public class AppController implements Initializable {
}
}
public void verifyDownload(ActionEvent event) {
if(downloadVerifierDialog != null) {
Stage stage = (Stage)downloadVerifierDialog.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
if(event.getSource() instanceof File file) {
downloadVerifierDialog.setSignatureFile(file);
}
return;
}
downloadVerifierDialog = new DownloadVerifierDialog(event.getSource() instanceof File file ? file : null);
downloadVerifierDialog.initOwner(rootStack.getScene().getWindow());
downloadVerifierDialog.showAndWait();
downloadVerifierDialog = null;
}
public void minimizeToTray(ActionEvent event) {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
@@ -3029,6 +3068,13 @@ public class AppController implements Initializable {
}
}
@Subscribe
public void requestVerifyDownloadOpen(RequestVerifyDownloadEvent event) {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
verifyDownload(new ActionEvent(event.getFile(), rootStack));
}
}
@Subscribe
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
@@ -33,6 +33,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
@@ -68,6 +69,8 @@ import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppServices {
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
@@ -78,6 +81,8 @@ public class AppServices {
private static final int RATES_PERIOD_SECS = 5 * 60;
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final int CONNECTION_DELAY_SECS = 2;
private static final int RATES_DELAY_SECS_DEFAULT = 2;
private static final int RATES_DELAY_SECS_WINDOWS = 5;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
@@ -355,6 +360,8 @@ public class AppServices {
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
//Delay startup on first run, Windows requires a longer delay
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true);
@@ -780,7 +787,7 @@ public class AppServices {
}
public static Window getActiveWindow() {
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : null);
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : (Stage.getWindows().iterator().hasNext() ? Stage.getWindows().iterator().next() : null));
}
public static void moveToActiveWindowScreen(Dialog<?> dialog) {
@@ -853,6 +860,25 @@ public class AppServices {
}
}
public static void openFileUriArgumentsAfterWalletLoading(Window window) {
if(!argFiles.isEmpty() || !argUris.isEmpty()) {
Service<Void> service = new Service<>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() {
Platform.runLater(() -> openFileUriArguments(window));
return null;
}
};
}
};
service.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
service.start();
}
}
public static void openFileUriArguments(Window window) {
openFiles(argFiles, window);
argFiles.clear();
@@ -879,6 +905,8 @@ public class AppServices {
for(File file : openFiles) {
if(isWalletFile(file)) {
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
} else if(isVerifyDownloadFile(file)) {
EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file));
} else {
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
}
@@ -12,7 +12,12 @@ import org.fxmisc.richtext.event.MouseOverTextEvent;
import org.fxmisc.richtext.model.TwoDimensional;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
public abstract class BaseController {
@@ -24,14 +29,11 @@ public abstract class BaseController {
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
if(!hoverChunk.isOpCode()) {
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeScriptChunk(hoverChunk));
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
}
ScriptChunk hoverChunk = getScriptChunk(scriptArea, e.getCharacterIndex());
if(hoverChunk != null) {
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeScriptChunk(hoverChunk));
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
}
});
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
@@ -80,4 +82,26 @@ public abstract class BaseController {
return "Invalid";
}
public static ScriptChunk getScriptChunk(ScriptArea area, int characterIndex) {
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(characterIndex, Backward);
int ignoreCount = 0;
for(int i = 0; i < position.getMajor() && i < area.getParagraph(0).getStyleSpans().getSpanCount(); i++) {
Collection<String> styles = area.getParagraph(0).getStyleSpans().getStyleSpan(i).getStyle();
if(i < position.getMajor() && (styles.contains("") || styles.contains("script-nest"))) {
ignoreCount++;
}
}
boolean hashScripts = List.of(P2PKH, P2SH, P2WPKH, P2WSH).stream().anyMatch(type -> type.isScriptType(area.getScript()));
List<ScriptChunk> flatChunks = area.getScript().getChunks().stream().flatMap(chunk -> !hashScripts && chunk.isScript() ? chunk.getScript().getChunks().stream() : Stream.of(chunk)).collect(Collectors.toList());
int chunkIndex = position.getMajor() - ignoreCount;
if(chunkIndex < flatChunks.size()) {
ScriptChunk chunk = flatChunks.get(chunkIndex);
if(!chunk.isOpCode()) {
return chunk;
}
}
return null;
}
}
@@ -80,6 +80,7 @@ public class SparrowDesktop extends Application {
}
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
mainStage.setWidth(Config.get().getAppWidth());
@@ -107,7 +108,7 @@ public class SparrowDesktop extends Application {
}
}
AppServices.openFileUriArguments(stage);
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
AppServices.get().start();
}
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.8.1";
public static final String APP_VERSION = "1.8.3";
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";
@@ -20,6 +20,8 @@ import java.util.List;
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
private static final int MAX_SHOWN_ACCOUNTS = 8;
private final ComboBox<StandardAccount> standardAccountCombo;
private boolean discoverAccounts = false;
@@ -55,7 +57,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount)) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount) && availableAccounts.size() <= MAX_SHOWN_ACCOUNTS) {
availableAccounts.add(standardAccount);
}
}
@@ -70,7 +70,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
if(transactionEntry.isConfirming()) {
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
@@ -119,6 +119,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
private static final class CoinTooltip extends Tooltip {
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
private boolean showConfirmations;
private boolean isCoinbase;
private String value;
public void setValue(String value) {
@@ -126,8 +127,9 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText();
}
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty, boolean coinbase) {
showConfirmations = true;
isCoinbase = coinbase;
int confirmations = txEntryConfirmationsProperty.get();
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
@@ -155,7 +157,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
if(confirmations == 0) {
return "Unconfirmed in mempool";
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s") + (isCoinbase ? ", immature coinbase" : "");
} else {
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
}
@@ -10,12 +10,16 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
private BitcoinUnit bitcoinUnit;
public CopyableCoinLabel() {
this("Unknown");
}
@@ -23,6 +27,21 @@ public class CopyableCoinLabel extends CopyableLabel {
public CopyableCoinLabel(String text) {
super(text);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> {
if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit();
}
if(bitcoinUnit == BitcoinUnit.SATOSHIS) {
bitcoinUnit = BitcoinUnit.BTC;
} else {
bitcoinUnit = BitcoinUnit.SATOSHIS;
}
refresh(Config.get().getUnitFormat(), bitcoinUnit);
});
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
@@ -63,6 +82,8 @@ public class CopyableCoinLabel extends CopyableLabel {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
this.bitcoinUnit = unit;
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue);
setText(btcValue);
@@ -0,0 +1,744 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.pgp.PGPKeySource;
import com.sparrowwallet.drongo.pgp.PGPUtils;
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.net.VersionCheckService;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppController.DRAG_OVER_CLASS;
public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class);
private static final DateFormat signatureDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z");
private static final long MAX_VALID_MANIFEST_SIZE = 100 * 1024;
private static final String SHA256SUMS_MANIFEST_PREFIX = "sha256sums";
private static final List<String> SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg");
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
private final ObjectProperty<File> signature = new SimpleObjectProperty<>();
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
private final Label signedBy;
private final Label releaseHash;
private final Label releaseVerified;
private final Hyperlink releaseLink;
private static File lastFileParent;
public DownloadVerifierDialog(File initialSignatureFile) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeader(new Header());
setupDrag(dialogPane);
VBox vBox = new VBox();
vBox.setSpacing(20);
vBox.setPadding(new Insets(20, 10, 10, 20));
Form form = new Form();
Fieldset filesFieldset = new Fieldset();
filesFieldset.setText("Files");
filesFieldset.setSpacing(10);
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null);
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
filesFieldset.getChildren().addAll(signatureField, manifestField, publicKeyField, releaseFileField);
form.getChildren().add(filesFieldset);
Fieldset resultsFieldset = new Fieldset();
resultsFieldset.setText("Results");
resultsFieldset.setSpacing(10);
signedBy = new Label();
Field signedByField = setupResultField(signedBy, "Signed By");
releaseHash = new Label();
Field hashMatchedField = setupResultField(releaseHash, "Release Hash");
releaseVerified = new Label();
Field releaseVerifiedField = setupResultField(releaseVerified, "Verified");
releaseLink = new Hyperlink("");
releaseVerifiedField.getInputs().add(releaseLink);
releaseLink.setOnAction(event -> {
if(release.get() != null && release.get().exists()) {
if(release.get().getName().toLowerCase(Locale.ROOT).startsWith("sparrow")) {
Optional<ButtonType> optType = AppServices.showAlertDialog("Exit Sparrow?", "Sparrow must be closed before installation. Exit?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
if(optType.isPresent() && optType.get() == ButtonType.YES) {
javafx.application.Platform.exit();
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
}
} else {
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
}
}
});
resultsFieldset.getChildren().addAll(signedByField, hashMatchedField, releaseVerifiedField);
form.getChildren().add(resultsFieldset);
vBox.getChildren().addAll(form);
dialogPane.setContent(vBox);
ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear", ButtonBar.ButtonData.CANCEL_CLOSE);
ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(clearButtonType, closeButtonType);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.CANCEL_CLOSE.equals(getResult())) {
signature.set(null);
manifest.set(null);
publicKey.set(null);
release.set(null);
signedBy.setText("");
signedBy.setGraphic(null);
releaseHash.setText("");
releaseHash.setGraphic(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
event.consume();
}
});
setResultConverter(ButtonType::getButtonData);
AppServices.moveToActiveWindowScreen(this);
dialogPane.setPrefWidth(900);
setResizable(true);
signature.addListener((observable, oldValue, signatureFile) -> {
if(signatureFile != null) {
boolean verify = true;
File actualSignatureFile = findSignatureFile(signatureFile);
if(actualSignatureFile != null && !actualSignatureFile.equals(signature.get())) {
signature.set(actualSignatureFile);
verify = false;
} else if(PGPUtils.signatureContainsManifest(signatureFile)) {
manifest.set(signatureFile);
verify = false;
} else {
File manifestFile = findManifestFile(signatureFile);
if(manifestFile != null && !manifestFile.equals(manifest.get())) {
manifest.set(manifestFile);
verify = false;
}
}
if(verify) {
verify();
}
}
});
manifest.addListener((observable, oldValue, manifestFile) -> {
if(manifestFile != null) {
boolean verify = true;
try {
Map<File, String> manifestMap = getManifest(manifestFile);
File releaseFile = findReleaseFile(manifestFile, manifestMap);
if(releaseFile != null && !releaseFile.equals(release.get())) {
release.set(releaseFile);
verify = false;
}
} catch(IOException e) {
log.debug("Error reading manifest file", e);
verify = false;
} catch(InvalidManifestException e) {
release.set(manifestFile);
verify = false;
}
if(verify) {
verify();
}
}
});
publicKey.addListener((observable, oldValue, newValue) -> {
verify();
});
release.addListener((observable, oldValue, releaseFile) -> {
verify();
});
if(initialSignatureFile != null) {
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
}
}
private void setupDrag(DialogPane dialogPane) {
dialogPane.setOnDragOver(event -> {
if(event.getGestureSource() != dialogPane && event.getDragboard().hasFiles()) {
event.acceptTransferModes(TransferMode.LINK);
}
event.consume();
});
dialogPane.setOnDragDropped(event -> {
Dragboard db = event.getDragboard();
boolean success = false;
if(db.hasFiles()) {
for(File file : db.getFiles()) {
if(isVerifyDownloadFile(file)) {
signature.set(file);
break;
}
}
success = true;
}
event.setDropCompleted(success);
event.consume();
});
dialogPane.setOnDragEntered(event -> {
dialogPane.getStyleClass().add(DRAG_OVER_CLASS);
});
dialogPane.setOnDragExited(event -> {
dialogPane.getStyleClass().removeAll(DRAG_OVER_CLASS);
});
}
private void verify() {
publicKeyDisabled.set(false);
if(signature.get() == null || manifest.get() == null) {
clearReleaseFields();
return;
}
PGPVerifyService pgpVerifyService = new PGPVerifyService(signature.get(), manifest.get(), publicKey.get());
pgpVerifyService.setOnRunning(event -> {
signedBy.setText("Verifying...");
signedBy.setGraphic(GlyphUtils.getBusyGlyph());
signedBy.setTooltip(null);
clearReleaseFields();
});
pgpVerifyService.setOnSucceeded(event -> {
PGPVerificationResult result = pgpVerifyService.getValue();
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
signedBy.setText(message);
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
publicKeyDisabled.set(true);
}
if(manifest.get().equals(release.get())) {
releaseHash.setText("No hash required, signature signs release file directly");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("Ready to install ");
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
releaseLink.setText(release.get().getName());
} else {
verifyManifest();
}
});
pgpVerifyService.setOnFailed(event -> {
Throwable e = event.getSource().getException();
signedBy.setText(getDisplayMessage(e));
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
clearReleaseFields();
});
pgpVerifyService.start();
}
private void clearReleaseFields() {
releaseHash.setText("");
releaseHash.setGraphic(null);
releaseHash.setTooltip(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
}
private void verifyManifest() {
File releaseFile = release.get();
if(releaseFile != null && releaseFile.exists()) {
FileSha256Service hashService = new FileSha256Service(releaseFile);
hashService.setOnRunning(event -> {
releaseHash.setText("Calculating...");
releaseHash.setGraphic(GlyphUtils.getBusyGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
});
hashService.setOnSucceeded(event -> {
String calculatedHash = hashService.getValue();
try {
Map<File, String> manifestMap = getManifest(manifest.get());
String manifestHash = getManifestHash(releaseFile.getName(), manifestMap);
if(calculatedHash.equalsIgnoreCase(manifestHash)) {
releaseHash.setText("Matched manifest hash");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
releaseHash.setTooltip(new Tooltip(calculatedHash));
releaseVerified.setText("Ready to install ");
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
releaseLink.setText(releaseFile.getName());
} else if(manifestHash == null) {
releaseHash.setText("Could not find manifest hash for " + releaseFile.getName());
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip("Manifest hashes provided for:\n" + manifestMap.keySet().stream().map(File::getName).collect(Collectors.joining("\n"))));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
} else {
releaseHash.setText("Did not match manifest hash");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip("Calculated Hash: " + calculatedHash + "\nManifest Hash: " + manifestHash));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
} catch(IOException | InvalidManifestException e) {
releaseHash.setText("Could not read manifest");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip(e.getMessage()));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
});
hashService.setOnFailed(event -> {
releaseHash.setText("Could not calculate manifest");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip(event.getSource().getException().getMessage()));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
});
hashService.start();
} else {
releaseHash.setText("No release file");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("Not verified");
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
}
private Field setupField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
Field field = new Field();
field.setText(title + ":");
FileField fileField = new FileField(fileProperty, title, extensions, optional, example, disabledProperty);
field.getInputs().add(fileField);
return field;
}
private Field setupResultField(Label label, String title) {
Field field = new Field();
field.setText(title + ":");
field.getInputs().add(label);
label.setGraphicTextGap(8);
return field;
}
public static Map<File, String> getManifest(File manifest) throws IOException, InvalidManifestException {
if(manifest.length() > MAX_VALID_MANIFEST_SIZE) {
throw new InvalidManifestException();
}
try(InputStream manifestStream = new FileInputStream(manifest)) {
return getManifest(manifestStream);
}
}
public static Map<File, String> getManifest(InputStream manifestStream) throws IOException {
Map<File, String> manifest = new HashMap<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(manifestStream, StandardCharsets.UTF_8));
String line;
while((line = reader.readLine()) != null) {
String[] parts = line.split("\\s+");
if(parts.length > 1 && parts[0].length() == 64) {
String manifestHash = parts[0];
String manifestFileName = parts[1];
if(manifestFileName.startsWith("*") || manifestFileName.startsWith("U") || manifestFileName.startsWith("^")) {
manifestFileName = manifestFileName.substring(1);
}
manifest.put(new File(manifestFileName), manifestHash);
}
}
return manifest;
}
private String getManifestHash(String contentFileName, Map<File, String> manifest) {
for(Map.Entry<File, String> entry : manifest.entrySet()) {
if(contentFileName.equalsIgnoreCase(entry.getKey().getName())) {
return entry.getValue();
}
}
return null;
}
private File findSignatureFile(File providedFile) {
for(String extension : SIGNATURE_EXTENSIONS) {
File signatureFile = new File(providedFile.getParentFile(), providedFile.getName() + "." + extension);
if(signatureFile.exists()) {
return signatureFile;
}
}
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) {
String version = matcher.group();
File signatureFile = new File(providedFile.getParentFile(), SPARROW_RELEASE_PREFIX + version + SPARROW_SIGNATURE_SUFFIX);
if(signatureFile.exists()) {
return signatureFile;
}
}
}
return null;
}
private File findManifestFile(File providedFile) {
String signatureName = providedFile.getName();
if(signatureName.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> signatureName.toLowerCase(Locale.ROOT).endsWith("." + ext))) {
File manifestFile = new File(providedFile.getParent(), signatureName.substring(0, signatureName.length() - 4));
if(manifestFile.exists()) {
return manifestFile;
}
}
return null;
}
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
List<String> releaseExtensions = getReleaseFileExtensions();
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
for(List<String> extensions : extensionLists) {
for(File file : manifestMap.keySet()) {
if(extensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
File releaseFile = new File(manifestFile.getParent(), file.getName());
if(releaseFile.exists()) {
return releaseFile;
}
}
}
}
return null;
}
private List<String> getReleaseFileExtensions() {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
return MACOS_RELEASE_EXTENSIONS;
}
case WINDOWS -> {
return WINDOWS_RELEASE_EXTENSIONS;
}
default -> {
return LINUX_RELEASE_EXTENSIONS;
}
}
}
private String getReleaseFileExample(String version) {
Platform platform = Platform.getCurrent();
String arch = System.getProperty("os.arch");
switch(platform) {
case OSX -> {
return "Sparrow-" + version + "-" + arch;
}
case WINDOWS -> {
return "Sparrow-" + version;
}
default -> {
return "sparrow_" + version + "-1_" + (arch.equals("aarch64") ? "arm64" : arch);
}
}
}
private String getDisplayMessage(Throwable e) {
String message = e.getMessage();
message = message.substring(0, 1).toUpperCase(Locale.ROOT) + message.substring(1);
if(message.endsWith(".")) {
message = message.substring(0, message.length() - 1);
}
if(message.equals("Invalid header encountered")) {
message += ", not a valid signature file";
}
if(message.startsWith("Malformed message")) {
message = "Not a valid signature file";
}
return message;
}
public static boolean isVerifyDownloadFile(File file) {
if(file != null) {
String name = file.getName().toLowerCase(Locale.ROOT);
if(name.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext))) {
return true;
}
if(MANIFEST_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext)) || name.startsWith(SHA256SUMS_MANIFEST_PREFIX)) {
try {
Map<File, String> manifest = getManifest(file);
return !manifest.isEmpty();
} catch(Exception e) {
//ignore
}
}
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
return matcher.find();
}
}
return false;
}
public void setSignatureFile(File signatureFile) {
signature.set(signatureFile);
}
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
getStyleClass().add("header-panel");
VBox vBox = new VBox();
vBox.setPadding(new Insets(10, 0, 0, 0));
Label headerLabel = new Label("Verify Download");
headerLabel.setWrapText(true);
headerLabel.setAlignment(Pos.CENTER_LEFT);
headerLabel.setMaxWidth(Double.MAX_VALUE);
headerLabel.setMaxHeight(Double.MAX_VALUE);
CopyableLabel descriptionLabel = new CopyableLabel("Download the release file, GPG signature and optional manifest of a project to verify the download integrity");
descriptionLabel.setAlignment(Pos.CENTER_LEFT);
vBox.getChildren().addAll(headerLabel, descriptionLabel);
add(vBox, 0, 0);
StackPane graphicContainer = new StackPane();
graphicContainer.getStyleClass().add("graphic-container");
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
graphicContainer.getChildren().add(imageView);
}
add(graphicContainer, 1, 0);
ColumnConstraints textColumn = new ColumnConstraints();
textColumn.setFillWidth(true);
textColumn.setHgrow(Priority.ALWAYS);
ColumnConstraints graphicColumn = new ColumnConstraints();
graphicColumn.setFillWidth(false);
graphicColumn.setHgrow(Priority.NEVER);
getColumnConstraints().setAll(textColumn , graphicColumn);
}
}
private static class FileField extends HBox {
private final ObjectProperty<File> fileProperty;
public FileField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
super(10);
this.fileProperty = fileProperty;
TextField textField = new TextField();
textField.setEditable(false);
textField.setPromptText("e.g. " + example + formatExtensionsList(extensions) + (optional ? " (optional)" : ""));
textField.setOnMouseClicked(event -> browseForFile(title, extensions));
Button browseButton = new Button("Browse...");
browseButton.setOnAction(event -> browseForFile(title, extensions));
getChildren().addAll(textField, browseButton);
HBox.setHgrow(textField, Priority.ALWAYS);
fileProperty.addListener((observable, oldValue, file) -> {
textField.setText(file == null ? "" : file.getAbsolutePath());
if(file != null) {
lastFileParent = file.getParentFile();
}
});
if(disabledProperty != null) {
disabledProperty.addListener((observable, oldValue, disabled) -> {
textField.setDisable(disabled);
browseButton.setDisable(disabled);
});
}
}
private void browseForFile(String title, List<String> extensions) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open File");
File userDir = new File(System.getProperty("user.home"));
File downloadsDir = new File(userDir, "Downloads");
fileChooser.setInitialDirectory(lastFileParent != null ? lastFileParent : (downloadsDir.exists() ? downloadsDir : userDir));
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(title + " files", extensions));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
fileProperty.set(file);
}
}
public String formatExtensionsList(List<String> items) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < items.size(); i++) {
result.append(".").append(items.get(i));
if (i < items.size() - 1) {
result.append(", ");
}
if (i == items.size() - 2) {
result.append("or ");
}
}
return result.toString();
}
}
private static class PGPVerifyService extends Service<PGPVerificationResult> {
private final File signature;
private final File manifest;
private final File publicKey;
public PGPVerifyService(File signature, File manifest, File publicKey) {
this.signature = signature;
this.manifest = manifest;
this.publicKey = publicKey;
}
@Override
protected Task<PGPVerificationResult> createTask() {
return new Task<>() {
protected PGPVerificationResult call() throws IOException, PGPVerificationException {
boolean detachedSignature = !manifest.equals(signature);
try(InputStream publicKeyStream = publicKey == null ? null : new FileInputStream(publicKey);
InputStream contentStream = new BufferedInputStream(new FileInputStream(manifest));
InputStream detachedSignatureStream = detachedSignature ? new FileInputStream(signature) : null) {
return PGPUtils.verify(publicKeyStream, contentStream, detachedSignatureStream);
}
}
};
}
}
private static class FileSha256Service extends Service<String> {
private final File file;
public FileSha256Service(File file) {
this.file = file;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
protected String call() throws IOException {
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return sha256(inputStream);
}
}
};
}
private String sha256(InputStream stream) throws IOException {
try {
final byte[] buffer = new byte[1024 * 1024];
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
int bytesRead = 0;
while((bytesRead = stream.read(buffer)) >= 0) {
if (bytesRead > 0) {
sha256.update(buffer, 0, bytesRead);
}
}
return Utils.bytesToHex(sha256.digest());
} catch(NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
private static class InvalidManifestException extends Exception { }
}
@@ -357,7 +357,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList());
if(ourOutputs.isEmpty()) {
AppServices.showErrorDialog("No spendable outputs", "None of the outputs on this transaction are spendable.\n\n Ensure that the outputs are not frozen" +
AppServices.showErrorDialog("No spendable outputs", "None of the outputs on this transaction are spendable.\n\nEnsure that the outputs are not frozen" +
(transactionEntry.getConfirmations() <= 0 ? ", and spending unconfirmed UTXOs is allowed." : "."));
return;
}
@@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
@@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
} else {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
UR ur = UR.fromBytes(baos.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
@@ -13,6 +14,8 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
@@ -122,16 +125,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
String walletPassword = password.get().asString();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
exportWallet(file, decryptedWallet, walletPassword);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
@@ -141,14 +140,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
decryptWalletService.start();
}
} else {
exportWallet(file, wallet);
exportWallet(file, wallet, null);
}
}
private void exportWallet(File file, Wallet exportWallet) {
private void exportWallet(File file, Wallet exportWallet, String password) {
try {
if(file != null) {
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet);
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
fileWalletExportService.setOnSucceeded(event -> {
EventManager.get().post(new WalletExportEvent(exportWallet));
});
@@ -163,12 +162,16 @@ public class FileWalletExportPane extends TitledDescriptionPane {
fileWalletExportService.start();
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(exportWallet, outputStream);
exporter.exportWallet(exportWallet, outputStream, password);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Bip129) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else if(exporter instanceof Descriptor) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
@@ -185,6 +188,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
} finally {
if(file == null && password != null) {
exportWallet.clearPrivate();
}
}
}
@@ -192,11 +199,13 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final WalletExport exporter;
private final File file;
private final Wallet wallet;
private final String password;
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet) {
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet, String password) {
this.exporter = exporter;
this.file = file;
this.wallet = wallet;
this.password = password;
}
@Override
@@ -205,7 +214,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
@Override
protected Void call() throws Exception {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(wallet, outputStream);
exporter.exportWallet(wallet, outputStream, password);
} finally {
if(password != null) {
wallet.clearPrivate();
}
}
return null;
@@ -190,6 +190,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
boolean isValidKey = isValidKey();
createButton.setDisable(!isValidKey || !isValidToAddress());
setScriptTypes(isValidKey);
if(isValidKey) {
setFromAddress();
}
@@ -289,6 +290,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
keyAddress.setText(address.toString());
}
private void setScriptTypes(boolean isValidKey) {
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
keyScriptType.getSelectionModel().select(0);
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
}
}
private void scanPrivateKey() {
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
@@ -1,22 +1,28 @@
package com.sparrowwallet.sparrow.control;
public enum QRDensity {
NORMAL("Normal", 400),
LOW("Low", 80);
NORMAL("Normal", 400, 2000),
LOW("Low", 80, 1000);
private final String name;
private final int maxFragmentLength;
private final int maxUrFragmentLength;
private final int maxBbqrFragmentLength;
QRDensity(String name, int maxFragmentLength) {
QRDensity(String name, int maxUrFragmentLength, int maxBbqrFragmentLength) {
this.name = name;
this.maxFragmentLength = maxFragmentLength;
this.maxUrFragmentLength = maxUrFragmentLength;
this.maxBbqrFragmentLength = maxBbqrFragmentLength;
}
public String getName() {
return name;
}
public int getMaxFragmentLength() {
return maxFragmentLength;
public int getMaxUrFragmentLength() {
return maxUrFragmentLength;
}
public int getMaxBbqrFragmentLength() {
return maxBbqrFragmentLength;
}
}
@@ -14,6 +14,9 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoder;
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoding;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.Node;
@@ -44,10 +47,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
private static final int DEFAULT_QR_SIZE = 580;
private static final int REDUCED_QR_SIZE = 520;
private static final BBQREncoding DEFAULT_BBQR_ENCODING = BBQREncoding.ZLIB;
private final int qrSize = getQRSize();
private final UR ur;
private UREncoder encoder;
private UREncoder urEncoder;
private final BBQR bbqr;
private BBQREncoder bbqrEncoder;
private boolean useBbqrEncoding;
private final ImageView qrImageView;
@@ -62,17 +71,26 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
private static boolean initialDensityChange;
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
this(UR.fromBytes(type, data), addLegacyEncodingOption, false);
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false);
}
public QRDisplayDialog(UR ur) {
this(ur, false, false);
this(ur, null, false, false, false);
}
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption, boolean addScanButton) {
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) {
this.ur = ur;
this.addLegacyEncodingOption = addLegacyEncodingOption;
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
this.bbqr = bbqr;
this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption;
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
if(bbqr != null) {
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
if(selectBbqrButton) {
useBbqrEncoding = true;
}
}
final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
@@ -85,16 +103,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
nextPart();
if(encoder.isSinglePart()) {
if(isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
createAnimateQRService();
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().add(cancelButtonType);
if(addLegacyEncodingOption) {
if(this.addLegacyEncodingOption) {
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
} else {
@@ -102,8 +120,13 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
dialogPane.getButtonTypes().add(densityButtonType);
}
if(bbqr != null) {
final ButtonType bbqrButtonType = new javafx.scene.control.ButtonType("Show BBQr", ButtonBar.ButtonData.BACK_PREVIOUS);
dialogPane.getButtonTypes().add(bbqrButtonType);
}
if(addScanButton) {
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().add(scanButtonType);
}
@@ -121,7 +144,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
public QRDisplayDialog(String data, boolean addScanButton) {
this.ur = null;
this.encoder = null;
this.bbqr = null;
this.urEncoder = null;
this.bbqrEncoder = null;
final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
@@ -134,11 +159,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
qrImageView.setImage(getQrCode(data));
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
if(addScanButton) {
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().add(scanButtonType);
}
@@ -163,9 +188,22 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
});
}
private boolean isSinglePart() {
if(useBbqrEncoding) {
return bbqrEncoder.isSinglePart();
} else if(!useLegacyEncoding) {
return urEncoder.isSinglePart();
} else {
return legacyParts.length == 1;
}
}
private void nextPart() {
if(!useLegacyEncoding) {
String fragment = encoder.nextPart();
if(useBbqrEncoding) {
String fragment = bbqrEncoder.nextPart();
currentPart = fragment.toUpperCase(Locale.ROOT);
} else if(!useLegacyEncoding) {
String fragment = urEncoder.nextPart();
currentPart = fragment.toUpperCase(Locale.ROOT);
} else {
currentPart = legacyParts[legacyPartIndex];
@@ -201,37 +239,23 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
this.legacyParts = legacyEncoder.encode();
this.useLegacyEncoding = true;
if(legacyParts.length == 1) {
if(animateQRService != null) {
animateQRService.cancel();
}
nextPart();
qrImageView.setImage(getQrCode(currentPart));
} else if(animateQRService == null) {
createAnimateQRService();
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
restartAnimation();
} catch(UR.InvalidTypeException e) {
//Can't happen
}
} else {
this.useLegacyEncoding = false;
restartAnimation();
}
}
if(encoder.isSinglePart()) {
if(animateQRService != null) {
animateQRService.cancel();
}
qrImageView.setImage(getQrCode(currentPart));
} else if(animateQRService == null) {
createAnimateQRService();
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
if(useBbqrEncoding) {
this.useBbqrEncoding = true;
restartAnimation();
} else {
this.useBbqrEncoding = false;
restartAnimation();
}
}
@@ -240,12 +264,28 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
animateQRService.cancel();
}
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
nextPart();
if(encoder.isSinglePart()) {
if(bbqr != null) {
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
}
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
restartAnimation();
}
private void restartAnimation() {
if(isSinglePart()) {
if(animateQRService != null) {
animateQRService.cancel();
}
nextPart();
qrImageView.setImage(getQrCode(currentPart));
} else {
} else if(animateQRService == null) {
createAnimateQRService();
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
}
@@ -290,7 +330,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(density, buttonData);
density.setOnAction(event -> {
if(!initialDensityChange && !encoder.isSinglePart()) {
if(!initialDensityChange && !isSinglePart()) {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
initialDensityChange = true;
@@ -306,12 +346,25 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
return density;
}
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) {
Button scanButton = (Button)super.createButton(buttonType);
scanButton.setGraphicTextGap(5);
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
return scanButton;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.BACK_PREVIOUS) {
ToggleButton bbqr = new ToggleButton(buttonType.getText());
bbqr.setGraphicTextGap(5);
bbqr.setGraphic(getGlyph(FontAwesome5.Glyph.QRCODE));
bbqr.setSelected(useBbqrEncoding);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(bbqr, buttonData);
bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseBbqrEncoding(newValue);
});
return bbqr;
}
return super.createButton(buttonType);
@@ -30,6 +30,8 @@ import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
@@ -63,8 +65,9 @@ import java.util.stream.IntStream;
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
private final URDecoder decoder;
private final LegacyURDecoder legacyDecoder;
private final URDecoder urDecoder;
private final LegacyURDecoder legacyUrDecoder;
private final BBQRDecoder bbqrDecoder;
private final WebcamService webcamService;
private List<String> parts;
@@ -80,8 +83,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
this.urDecoder = new URDecoder();
this.legacyUrDecoder = new LegacyURDecoder();
this.bbqrDecoder = new BBQRDecoder();
if(Config.get().isHdCapture()) {
webcamResolutionProperty.set(WebcamResolution.HD);
@@ -167,7 +171,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
Platform.runLater(() -> webcamResolutionProperty.set(null));
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
@@ -192,23 +196,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
legacyDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
legacyUrDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(legacyUrDecoder.getPercentComplete()));
if(legacyDecoder.isComplete()) {
if(legacyUrDecoder.isComplete()) {
try {
UR ur = legacyDecoder.decode();
UR ur = legacyUrDecoder.decode();
result = extractResultFromUR(ur);
} catch(Exception e) {
result = new Result(new URException(e.getMessage()));
}
}
} else {
decoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
urDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(urDecoder.getProcessedPartsCount() > 0 ? urDecoder.getEstimatedPercentComplete() : 0));
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urDecoder.getResult() != null) {
URDecoder.Result urResult = urDecoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
result = extractResultFromUR(urResult.ur);
Platform.runLater(() -> setResult(result));
@@ -217,6 +221,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
}
} else if(BBQRDecoder.isBBQRFragment(qrtext)) {
bbqrDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(bbqrDecoder.getPercentComplete()));
if(bbqrDecoder.getResult() != null) {
BBQRDecoder.Result bbqrResult = bbqrDecoder.getResult();
if(bbqrResult.getResultType() == BBQRDecoder.ResultType.SUCCESS) {
result = extractResultFromBBQR(bbqrResult);
Platform.runLater(() -> setResult(result));
} else {
result = new Result(new BBQRException(bbqrResult.getError()));
}
}
} else if(partMatcher.matches()) {
int m = Integer.parseInt(partMatcher.group(1));
int n = Integer.parseInt(partMatcher.group(2));
@@ -442,6 +459,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(cryptoBip39);
return new Result(seed);
} else if(urRegistryType.equals(RegistryType.PSBT)) {
URPSBT urPSBT = (URPSBT)ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(urPSBT.getPsbt(), false);
return new Result(psbt);
} catch(Exception e) {
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
}
} else if(urRegistryType.equals(RegistryType.ADDRESS)) {
URAddress urAddress = (URAddress)ur.decodeFromRegistry();
Address address = getAddress(urAddress);
if(address != null) {
return new Result(BitcoinURI.fromAddress(address));
} else {
return new Result(new URException("Unknown " + urRegistryType + " type of " + urAddress.getType()));
}
} else if(urRegistryType.equals(RegistryType.HDKEY)) {
URHDKey urHDKey = (URHDKey)ur.decodeFromRegistry();
ExtendedKey extendedKey = getExtendedKey(urHDKey);
return new Result(extendedKey, urHDKey.getName());
} else if(urRegistryType.equals(RegistryType.OUTPUT_DESCRIPTOR)) {
UROutputDescriptor urOutputDescriptor = (UROutputDescriptor)ur.decodeFromRegistry();
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
return new Result(outputDescriptor);
} else if(urRegistryType.equals(RegistryType.ACCOUNT_DESCRIPTOR)) {
URAccountDescriptor urAccountDescriptor = (URAccountDescriptor)ur.decodeFromRegistry();
List<Wallet> wallets = getWallets(urAccountDescriptor);
return new Result(wallets);
} else if(urRegistryType.equals(RegistryType.SEED)) {
URSeed urSeed = (URSeed)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(urSeed);
return new Result(seed);
} else {
log.error("Unsupported UR type " + urRegistryType);
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
@@ -584,6 +634,53 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
}
private OutputDescriptor getOutputDescriptor(UROutputDescriptor urOutputDescriptor) {
String source = urOutputDescriptor.getSource();
List<RegistryItem> keys = urOutputDescriptor.getKeys();
Map<ExtendedKey, String> mapExtendedPublicKeyLabels = new LinkedHashMap<>();
for(int i = 0; i < keys.size(); i++) {
RegistryItem key = keys.get(i);
if(key instanceof URHDKey urhdKey) {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
}
} else {
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
}
}
return OutputDescriptor.getOutputDescriptor(source, mapExtendedPublicKeyLabels);
}
private List<Wallet> getWallets(URAccountDescriptor urAccountDescriptor) {
List<Wallet> wallets = new ArrayList<>();
String masterFingerprint = Utils.bytesToHex(urAccountDescriptor.getMasterFingerprint());
for(UROutputDescriptor urOutputDescriptor : urAccountDescriptor.getOutputDescriptors()) {
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
wallets.add(wallet);
}
return wallets;
}
private Result extractResultFromBBQR(BBQRDecoder.Result result) {
if(result.getPsbt() != null) {
return new Result(result.getPsbt());
} else if(result.getTransaction() != null) {
return new Result(result.getTransaction());
} else if(result.toString() != null) {
return new Result(result.toString());
} else {
log.error("Unsupported BBQR type " + result.getBbqrType());
return new Result(new URException("BBQR type " + result.getBbqrType() + " is not supported"));
}
}
}
private class QRScanListener implements WebcamListener {
@@ -58,6 +58,8 @@ public class ScriptArea extends CodeArea {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isTaprootControlBlock()) {
append("<controlblock>", "script-controlblock");
} else if(chunk.isString()) {
append(chunk.toString(), "script-other");
} else if(chunk.isScript()) {
@@ -2,25 +2,22 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.BaseController;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.model.TwoDimensional;
import java.util.OptionalInt;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
public class ScriptContextMenu extends ContextMenu {
private Script script;
private MenuItem copyvalue;
private ScriptChunk hoverChunk;
public ScriptContextMenu(CodeArea area, Script script)
public ScriptContextMenu(ScriptArea area, Script script)
{
this.script = script;
@@ -40,12 +37,9 @@ public class ScriptContextMenu extends ContextMenu {
Point2D point = area.screenToLocal(event.getScreenX(), event.getScreenY());
OptionalInt characterIndex = area.hit(point.getX(), point.getY()).getCharacterIndex();
if(characterIndex.isPresent()) {
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(characterIndex.getAsInt(), Backward);
if(position.getMajor() % 2 == 0) {
ScriptChunk chunk = script.getChunks().get(position.getMajor() / 2);
if(!chunk.isOpCode()) {
this.hoverChunk = chunk;
}
ScriptChunk chunk = BaseController.getScriptChunk(area, characterIndex.getAsInt());
if(chunk != null) {
this.hoverChunk = chunk;
}
}
copyvalue.setDisable(hoverChunk == null);
@@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
@@ -44,7 +45,6 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
@@ -56,6 +56,8 @@ import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.glyphfont.GlyphUtils.*;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
@@ -89,7 +91,7 @@ public class TransactionDiagram extends GridPane {
stage.setResizable(false);
StackPane scenePane = new StackPane();
if(Platform.getCurrent() == Platform.WINDOWS) {
if(Platform.getCurrent() == Platform.WINDOWS || Platform.getCurrent() == Platform.UNIX) {
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}
@@ -474,7 +476,7 @@ public class TransactionDiagram extends GridPane {
tooltip.setText("");
} else if(input instanceof AddUserBlockTransactionHashIndex) {
tooltip.setText("");
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph());
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
label.setOnMouseClicked(event -> {
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
closeExpanded();
@@ -686,19 +688,18 @@ public class TransactionDiagram extends GridPane {
List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) {
Glyph outputGlyph = getOutputGlyph(payment);
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
payment.setLabel(getOutputLabel(payment));
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = getToWallet(payment);
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
+ (isDuplicateAddress(payment) ? " (Duplicate)" : ""));
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE);
@@ -720,6 +721,7 @@ public class TransactionDiagram extends GridPane {
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
}
Set<Integer> seenIndexes = new HashSet<>();
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
WalletNode changeNode = changeEntry.getKey();
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
@@ -765,11 +767,15 @@ public class TransactionDiagram extends GridPane {
actionBox.getChildren().addAll(region, amountLabel);
}
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
if(isFinal()) {
Collections.sort(outputNodes);
int changeIndex = outputNodes.size();
if(isFinal()) {
changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes);
seenIndexes.add(changeIndex);
if(changeIndex > outputNodes.size()) {
changeIndex = outputNodes.size();
}
}
outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
for(OutputNode outputNode : outputNodes) {
@@ -783,7 +789,7 @@ public class TransactionDiagram extends GridPane {
}
boolean highFee = (walletTx.getFeePercentage() > 0.1);
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
@@ -923,38 +929,10 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
private String getOutputLabel(Payment payment) {
if(payment.getLabel() != null) {
return payment.getLabel();
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank change";
}
return null;
}
private int getOutputIndex(Address address, long amount) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
if(addressNode != null) {
return addressNode.getWallet();
}
}
}
return null;
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
return addressOutputs.indexOf(output);
}
private Wallet getBip47SendWallet(Payment payment) {
@@ -974,164 +952,6 @@ public class TransactionDiagram extends GridPane {
return null;
}
public Glyph getOutputGlyph(Payment payment) {
if(payment.getType().equals(Payment.Type.MIX)) {
return getMixGlyph();
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
} else if(walletTx.isBadbankSend(payment)) {
return getBadbankGlyph();
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this);
} else if(getToWallet(payment) != null) {
return getDepositGlyph();
} else if(isDuplicateAddress(payment)) {
return getPaymentWarningGlyph();
}
return getPaymentGlyph();
}
private boolean isDuplicateAddress(Payment payment) {
return walletTx.getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
excludeGlyph.setFontSize(12);
return excludeGlyph;
}
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
public static Glyph getPaymentWarningGlyph() {
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
paymentWarningGlyph.setFontSize(12);
return paymentWarningGlyph;
}
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
premixGlyph.setFontSize(12);
return premixGlyph;
}
public static Glyph getBadbankGlyph() {
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
badbankGlyph.getStyleClass().add("badbank-icon");
badbankGlyph.setFontSize(12);
return badbankGlyph;
}
public static Glyph getWhirlpoolFeeGlyph() {
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
whirlpoolFeeGlyph.setFontSize(12);
return whirlpoolFeeGlyph;
}
public static Glyph getFakeMixGlyph() {
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
fakeMixGlyph.getStyleClass().add("fakemix-icon");
fakeMixGlyph.setFontSize(12);
return fakeMixGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
public static Glyph getMixGlyph() {
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
payjoinGlyph.getStyleClass().add("mix-icon");
payjoinGlyph.setFontSize(12);
return payjoinGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
public static Glyph getChangeWarningGlyph() {
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
changeWarningGlyph.getStyleClass().add("change-warning-icon");
changeWarningGlyph.setFontSize(12);
return changeWarningGlyph;
}
public static Glyph getChangeReplaceGlyph() {
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
changeReplaceGlyph.setFontSize(12);
return changeReplaceGlyph;
}
public Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
private Glyph getWarningGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getQuestionGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
feeWarningGlyph.getStyleClass().add("question-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
private Glyph getUserGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
userGlyph.getStyleClass().add("user-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
private Glyph getUserAddGlyph() {
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
userAddGlyph.getStyleClass().add("useradd-icon");
@@ -1288,7 +1108,7 @@ public class TransactionDiagram extends GridPane {
}
}
private static class AdditionalPayment extends Payment {
public static class AdditionalPayment extends Payment {
private final List<Payment> additionalPayments;
public AdditionalPayment(List<Payment> additionalPayments) {
@@ -1296,10 +1116,10 @@ public class TransactionDiagram extends GridPane {
this.additionalPayments = additionalPayments;
}
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
public Glyph getOutputGlyph(WalletTransaction walletTx) {
Glyph glyph = null;
for(Payment payment : additionalPayments) {
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
Glyph paymentGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
return getPaymentGlyph();
}
@@ -1315,7 +1135,7 @@ public class TransactionDiagram extends GridPane {
}
}
private class OutputNode implements Comparable<OutputNode> {
private static class OutputNode {
public Pane outputLabel;
public Address address;
public long amount;
@@ -1325,15 +1145,6 @@ public class TransactionDiagram extends GridPane {
this.address = address;
this.amount = amount;
}
@Override
public int compareTo(TransactionDiagram.OutputNode o) {
try {
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
} catch(Exception e) {
return 0;
}
}
}
private class LabelContextMenu extends ContextMenu {
@@ -1,7 +1,9 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
@@ -81,13 +83,13 @@ public class TransactionDiagramLabel extends HBox {
List<Payment> badbankOutputs = walletTx.getPayments().stream().filter(walletTx::isBadbankSend).collect(Collectors.toList());
List<OutputLabel> badbankOutputLabels = badbankOutputs.stream().map(payment -> getBadbankOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
outputLabels.addAll(badbankOutputLabels);
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_PREMIX && walletTx.getPayments().stream().anyMatch(walletTx::isPostmixSend)) {
OutputLabel mixOutputLabel = getMixOutputLabel(transactionDiagram, walletTx.getPayments());
if(mixOutputLabel != null) {
outputLabels.add(mixOutputLabel);
}
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) {
@@ -142,7 +144,7 @@ public class TransactionDiagramLabel extends HBox {
Payment premixOutput = premixOutputs.get(0);
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = transactionDiagram.getOutputGlyph(premixOutput);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), premixOutput);
String text;
if(premixOutputs.size() == 1) {
text = "Premix transaction with 1 output of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats";
@@ -155,7 +157,7 @@ public class TransactionDiagramLabel extends HBox {
}
private OutputLabel getBadbankOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = "Badbank change of " + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
@@ -164,7 +166,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getWhirlpoolFeeOutputLabel(TransactionDiagram transactionDiagram, Payment whirlpoolFee, List<Payment> premixOutputs) {
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
double feePercentage = (double)whirlpoolFee.getAmount() / (total - whirlpoolFee.getAmount());
Glyph glyph = transactionDiagram.getOutputGlyph(whirlpoolFee);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), whirlpoolFee);
String text = "Whirlpool fee of " + transactionDiagram.getSatsValue(whirlpoolFee.getAmount()) + " sats (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
return getOutputLabel(glyph, text);
@@ -177,7 +179,7 @@ public class TransactionDiagramLabel extends HBox {
Payment remixOutput = mixOutputs.get(0);
long total = mixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
Glyph glyph = GlyphUtils.getPremixGlyph();
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
@@ -191,7 +193,7 @@ public class TransactionDiagramLabel extends HBox {
Payment remixOutput = remixOutputs.get(0);
long total = remixOutputs.stream().mapToLong(Payment::getAmount).sum();
Glyph glyph = TransactionDiagram.getPremixGlyph();
Glyph glyph = GlyphUtils.getPremixGlyph();
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
+ transactionDiagram.getSatsValue(total) + " sats)";
@@ -200,10 +202,10 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = transactionDiagram.getToWallet(payment);
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
return getOutputLabel(glyph, text);
@@ -212,7 +214,7 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Glyph glyph = TransactionDiagram.getChangeGlyph();
Glyph glyph = GlyphUtils.getChangeGlyph();
String text = "Change of " + transactionDiagram.getSatsValue(changeEntry.getValue()) + " sats to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
return getOutputLabel(glyph, text);
@@ -224,7 +226,7 @@ public class TransactionDiagramLabel extends HBox {
return null;
}
Glyph glyph = transactionDiagram.getFeeGlyph();
Glyph glyph = GlyphUtils.getFeeGlyph();
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
return getOutputLabel(glyph, text);
@@ -27,7 +27,8 @@ public class UsbStatusButton extends MenuButton {
public void setDevices(List<Device> devices) {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T ||
device.getModel() == WalletModel.TREZOR_SAFE_3 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
toggleItem.setOnAction(event -> {
@@ -51,7 +51,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault());
for(KeystoreFileImport importer : keystoreImporters) {
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
@@ -5,6 +5,7 @@ import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import com.sparrowwallet.bokmakierie.Bokmakierie;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@@ -43,6 +44,7 @@ public class WebcamService extends ScheduledService<Image> {
private Webcam cam;
private long lastQrSampleTime;
private final Reader qrReader;
private final Bokmakierie bokmakierie;
static {
Webcam.setDriver(new WebcamScanDriver());
@@ -55,6 +57,7 @@ public class WebcamService extends ScheduledService<Image> {
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader();
this.bokmakierie = new Bokmakierie();
}
@Override
@@ -156,6 +159,15 @@ public class WebcamService extends ScheduledService<Image> {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
if(result != null) {
return new Result(result.getMessage(), result.getRawBytes(), new ResultPoint[0], BarcodeFormat.QR_CODE);
}
} catch(Exception e) {
log.debug("Error scanning QR", e);
}
if(ZBar.isEnabled()) {
ZBar.Scan scan = ZBar.scan(bufferedImage);
if(scan != null) {
@@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.event;
import javafx.stage.Window;
import java.io.File;
public class RequestVerifyDownloadEvent {
private final Window window;
private final File file;
public RequestVerifyDownloadEvent(Window window) {
this.window = window;
this.file = null;
}
public RequestVerifyDownloadEvent(Window window, File file) {
this.window = window;
this.file = file;
}
public Window getWindow() {
return window;
}
public File getFile() {
return file;
}
}
@@ -44,6 +44,7 @@ public class FontAwesome5 extends GlyphFont {
HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'),
HAND_HOLDING_WATER('\uf4c1'),
HOURGLASS_HALF('\uf252'),
HISTORY('\uf1da'),
INFO_CIRCLE('\uf05a'),
KEY('\uf084'),
@@ -51,8 +52,10 @@ public class FontAwesome5 extends GlyphFont {
LINK('\uf0c1'),
LOCK('\uf023'),
LOCK_OPEN('\uf3c1'),
LONG_ARROW_ALT_RIGHT('\uf30b'),
MAGNIFYING_GLASS_PLUS('\uf00e'),
MAGNIFYING_GLASS_MINUS('\uf010'),
MICROCHIP('\uf2db'),
MINUS_CIRCLE('\uf056'),
PEN_FANCY('\uf5ac'),
PLUS('\uf067'),
@@ -0,0 +1,206 @@
package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
public class GlyphUtils {
public static Glyph getOutputGlyph(WalletTransaction walletTx, Payment payment) {
if(payment.getType().equals(Payment.Type.MIX)) {
return getMixGlyph();
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
} else if(walletTx.isBadbankSend(payment)) {
return getBadbankGlyph();
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof TransactionDiagram.AdditionalPayment) {
return ((TransactionDiagram.AdditionalPayment)payment).getOutputGlyph(walletTx);
} else if(walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment) != null) {
return getDepositGlyph();
} else if(walletTx.isDuplicateAddress(payment)) {
return getPaymentWarningGlyph();
}
return getPaymentGlyph();
}
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
public static Glyph getPaymentWarningGlyph() {
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
paymentWarningGlyph.setFontSize(12);
return paymentWarningGlyph;
}
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
premixGlyph.setFontSize(12);
return premixGlyph;
}
public static Glyph getBadbankGlyph() {
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
badbankGlyph.getStyleClass().add("badbank-icon");
badbankGlyph.setFontSize(12);
return badbankGlyph;
}
public static Glyph getWhirlpoolFeeGlyph() {
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
whirlpoolFeeGlyph.setFontSize(12);
return whirlpoolFeeGlyph;
}
public static Glyph getFakeMixGlyph() {
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
fakeMixGlyph.getStyleClass().add("fakemix-icon");
fakeMixGlyph.setFontSize(12);
return fakeMixGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
public static Glyph getMixGlyph() {
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
payjoinGlyph.getStyleClass().add("mix-icon");
payjoinGlyph.setFontSize(12);
return payjoinGlyph;
}
public static Glyph getExternalInputGlyph() {
Glyph externalGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LONG_ARROW_ALT_RIGHT);
externalGlyph.getStyleClass().add("external-input-icon");
externalGlyph.setFontSize(12);
return externalGlyph;
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
excludeGlyph.setFontSize(12);
return excludeGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
public static Glyph getChangeWarningGlyph() {
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
changeWarningGlyph.getStyleClass().add("change-warning-icon");
changeWarningGlyph.setFontSize(12);
return changeWarningGlyph;
}
public static Glyph getChangeReplaceGlyph() {
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
changeReplaceGlyph.setFontSize(12);
return changeReplaceGlyph;
}
public static Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
public static Glyph getFeeWarningGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
public static Glyph getQuestionGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
feeWarningGlyph.getStyleClass().add("question-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
public static Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
public static Glyph getUserGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
userGlyph.getStyleClass().add("user-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
public static Glyph getOpcodeGlyph() {
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.MICROCHIP);
userGlyph.getStyleClass().add("opcode-icon");
userGlyph.setFontSize(12);
return userGlyph;
}
public static Glyph getSuccessGlyph() {
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
successGlyph.getStyleClass().add("success");
successGlyph.setFontSize(12);
return successGlyph;
}
public static Glyph getWarningGlyph() {
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warningGlyph.getStyleClass().add("warn-icon");
warningGlyph.setFontSize(12);
return warningGlyph;
}
public static Glyph getFailureGlyph() {
Glyph failureGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
failureGlyph.getStyleClass().add("failure");
failureGlyph.setFontSize(12);
return failureGlyph;
}
public static Glyph getBusyGlyph() {
Glyph busyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HOURGLASS_HALF);
busyGlyph.getStyleClass().add("busy");
busyGlyph.setFontSize(12);
return busyGlyph;
}
}
@@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class AirGapVault extends KeystoneSinglesig {
@Override
public String getName() {
return "AirGap Vault";
}
@Override
public String getKeystoreImportDescription(int account) {
return "Import QR created on your AirGap Vault, by selecting the Secret > Account > Sparrow Wallet";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.AIRGAP_VAULT;
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
@Override
public boolean isWalletImportFileFormatAvailable() {
return false;
}
@Override
public boolean isDeprecated() {
return true;
}
}
@@ -189,7 +189,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) +
@@ -94,7 +94,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
@@ -62,7 +62,7 @@ public abstract class CardApi {
}
public static CardApi getCardApi(WalletModel walletModel, String pin) throws CardException {
if(walletModel == WalletModel.TAPSIGNER || walletModel == WalletModel.SATSCARD) {
if(walletModel == WalletModel.TAPSIGNER || walletModel == WalletModel.SATSCHIP || walletModel == WalletModel.SATSCARD) {
return new CkCardApi(walletModel, pin);
}
@@ -165,7 +165,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
@@ -224,17 +224,17 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public boolean isWalletImportScannable() {
return false;
return true;
}
@Override
public boolean isKeystoreImportScannable() {
return false;
return true;
}
@Override
public boolean isWalletExportScannable() {
return false;
return true;
}
@Override
@@ -45,12 +45,12 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
@Override
public boolean isWalletImportScannable() {
return false;
return true;
}
@Override
public boolean isKeystoreImportScannable() {
return false;
return true;
}
@Override
@@ -23,7 +23,7 @@ public class Descriptor implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("# Receive and change descriptor (BIP389):");
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
@@ -18,7 +15,10 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
@@ -301,11 +301,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getExportFileExtension(Wallet wallet) {
return "json";
return wallet.isEncrypted() ? "" : "json";
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
ElectrumJsonWallet ew = new ElectrumJsonWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
@@ -342,7 +342,18 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
ek.seed = keystore.getSeed().getMnemonicString().asString();
ek.passphrase = keystore.getSeed().getPassphrase() == null ? null : keystore.getSeed().getPassphrase().asString();
}
ew.use_encryption = false;
if(password != null) {
ek.xprv = encrypt(ek.xprv, password);
if(ek.seed != null) {
ek.seed = encrypt(ek.seed, password);
}
if(ek.passphrase != null) {
ek.passphrase = encrypt(ek.passphrase, password);
}
ew.use_encryption = true;
} else {
ew.use_encryption = false;
}
} else if(keystore.getSource() == KeystoreSource.SW_WATCH) {
ek.type = "bip32";
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
@@ -373,14 +384,41 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(eJson);
if(password != null) {
ECKey encryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password);
outputStream = new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionKey));
}
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error("Error exporting Electrum Wallet", e);
throw new ExportException("Error exporting Electrum Wallet", e);
}
}
private String encrypt(String plain, String password) throws NoSuchAlgorithmException {
if(plain == null) {
return null;
}
byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8);
KeyDeriver keyDeriver = new DoubleSha256KeyDeriver();
Key key = keyDeriver.deriveKey(password);
KeyCrypter keyCrypter = new AESKeyCrypter();
byte[] initializationVector = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(initializationVector);
EncryptedData encryptedData = keyCrypter.encrypt(plainBytes, initializationVector, key);
byte[] encrypted = new byte[initializationVector.length + encryptedData.getEncryptedBytes().length];
System.arraycopy(initializationVector, 0, encrypted, 0, 16);
System.arraycopy(encryptedData.getEncryptedBytes(), 0, encrypted, 16, encryptedData.getEncryptedBytes().length);
byte[] encryptedBase64 = Base64.getEncoder().encode(encrypted);
return new String(encryptedBase64, StandardCharsets.UTF_8);
}
@Override
public boolean isEncrypted(File file) {
return (FileType.BINARY.equals(IOUtils.getFileType(file)));
@@ -29,7 +29,7 @@ public class ElectrumPersonalServer implements WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(wallet.getScriptType() == ScriptType.P2TR) {
throw new ExportException(getName() + " does not support Taproot wallets.");
}
@@ -13,6 +13,9 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,19 +27,18 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String HWI_HOME_DIR = "hwi";
private static final String HWI_VERSION_PREFIX = "hwi-";
private static final String HWI_VERSION = "2.3.1";
private static final String HWI_VERSION = "2.4.0";
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
private static boolean isPromptActive = false;
@@ -377,9 +379,9 @@ public class Hwi {
if(platform == Platform.OSX) {
InputStream inputStream;
if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.zip");
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
} else {
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
}
if(inputStream == null) {
@@ -396,26 +398,27 @@ public class Hwi {
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
File hwiExec = null;
try(ZipInputStream zis = new ZipInputStream(inputStream)) {
ZipEntry zipEntry = zis.getNextEntry();
while(zipEntry != null) {
if(zipEntry.isDirectory()) {
newDirectory(hwiVersionDir, zipEntry, ownerExecutableWritable);
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
TarArchiveEntry tarEntry = tarInput.getNextEntry();
while(tarEntry != null) {
if(tarEntry.isDirectory()) {
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else if(tarEntry.isSymbolicLink()) {
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
} else {
File newFile = newFile(hwiVersionDir, zipEntry, ownerExecutableWritable);
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
try(FileOutputStream fos = new FileOutputStream(newFile)) {
ByteStreams.copy(zis, new FileOutputStream(newFile));
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
fos.flush();
};
}
if(zipEntry.getName().equals("hwi")) {
if(tarEntry.getName().equals("hwi")) {
hwiExec = newFile;
}
}
zipEntry = zis.getNextEntry();
tarEntry = tarInput.getNextEntry();
}
zis.closeEntry();
}
hwiExecutable = hwiExec;
@@ -478,29 +481,43 @@ public class Hwi {
}
}
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, zipEntry.getName());
Path path = Path.of(destDirPath, tarEntry.getName());
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destSubDirPath = destDir.getCanonicalPath();
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destDir;
}
public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, zipEntry.getName());
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
}
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, tarEntry.getName());
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
String destFilePath = destFile.getCanonicalPath();
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
}
return destFile;
@@ -836,7 +853,7 @@ public class Hwi {
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String modelStr = json.getAsJsonPrimitive().getAsString();
try {
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT));
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
} catch(Exception e) {
for(WalletModel model : WalletModel.values()) {
if(modelStr.startsWith(model.getType())) {
@@ -17,9 +17,4 @@ public class Krux extends SpecterDIY {
public WalletModel getWalletModel() {
return WalletModel.KRUX;
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
}
@@ -28,7 +28,7 @@ public class Sparrow implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
PersistenceType persistenceType = PersistenceType.DB;
@@ -65,7 +65,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
@@ -18,7 +18,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getFullName();
@@ -108,7 +108,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
WalletModel walletModel = device.getWalletModel();
if(walletModel != null) {
keystore.setWalletModel(walletModel);
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.KEEPKEY ||
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.TREZOR_SAFE_3 || walletModel == WalletModel.KEEPKEY ||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X || walletModel == WalletModel.LEDGER_NANO_S_PLUS ||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
keystore.setSource(KeystoreSource.HW_USB);
@@ -752,7 +752,7 @@ public class Storage {
protected Void call() throws IOException, ExportException {
Sparrow export = new Sparrow();
try(BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(newWalletFile))) {
export.exportWallet(wallet, outputStream);
export.exportWallet(wallet, outputStream, null);
}
return null;
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.OutputStream;
public interface WalletExport extends ImportExport {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension(Wallet wallet);
boolean isWalletExportScannable();
@@ -46,7 +46,7 @@ public class WalletLabels implements WalletImport, WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
for(Wallet exportWallet : allWallets) {
@@ -10,7 +10,6 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.control.EntryCell;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
@@ -21,11 +20,14 @@ import org.apache.commons.lang3.time.DateUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class WalletTransactions implements WalletExport {
private static final long ONE_DAY = 24*60*60*1000L;
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final WalletForm walletForm;
@@ -44,7 +46,7 @@ public class WalletTransactions implements WalletExport {
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
ExchangeSource exchangeSource = Config.get().getExchangeSource();
@@ -83,7 +85,7 @@ public class WalletTransactions implements WalletExport {
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getLabel());
writer.write(getCoinValue(bitcoinUnit, txEntry.getValue()));
writer.write(getCoinValue(bitcoinUnit, txEntry.getBalance()));
@@ -0,0 +1,3 @@
package com.sparrowwallet.sparrow.io.bbqr;
public record BBQR(BBQRType type, byte[] data) {}
@@ -0,0 +1,180 @@
package com.sparrowwallet.sparrow.io.bbqr;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
public class BBQRDecoder {
private static final Logger log = LoggerFactory.getLogger(BBQRDecoder.class);
private final Map<Integer, byte[]> receivedParts = new TreeMap<>();
private int totalParts;
private BBQRType type;
private Result result;
public static boolean isBBQRFragment(String part) {
try {
BBQRHeader.fromString(part);
return true;
} catch(Exception e) {
return false;
}
}
public void receivePart(String part) {
try {
BBQRHeader header = BBQRHeader.fromString(part);
totalParts = header.seqTotal();
type = header.type();
byte[] partData = header.decode(part);
receivedParts.put(header.seqNumber(), partData);
if(receivedParts.size() == totalParts) {
byte[] data = concatParts();
if(type == BBQRType.PSBT) {
result = new Result(new PSBT(data));
} else if(type == BBQRType.TXN) {
result = new Result(new Transaction(data));
} else if(type == BBQRType.JSON || type == BBQRType.UNICODE) {
result = new Result(type, new String(data, StandardCharsets.UTF_8));
} else {
result = new Result(type, data);
}
}
} catch(Exception e) {
log.error("Could not parse received QR of type " + type, e);
result = new Result(ResultType.FAILURE, e.getMessage());
}
}
private byte[] concatParts() {
int totalLength = 0;
for(byte[] part : receivedParts.values()) {
totalLength += part.length;
}
byte[] data = new byte[totalLength];
int index = 0;
for(byte[] part : receivedParts.values()) {
System.arraycopy(part, 0, data, index, part.length);
index += part.length;
}
return data;
}
public int getProcessedPartsCount() {
return receivedParts.size();
}
public double getPercentComplete() {
if(totalParts == 0) {
return 0d;
}
return (double)getProcessedPartsCount() / totalParts;
}
public Result getResult() {
return result;
}
public static class Result {
private final ResultType resultType;
private final BBQRType bbqrType;
private final PSBT psbt;
private final Transaction transaction;
private final String strData;
private final byte[] data;
private final String error;
public Result(ResultType resultType, String error) {
this.resultType = resultType;
this.bbqrType = null;
this.psbt = null;
this.transaction = null;
this.strData = null;
this.data = null;
this.error = error;
}
public Result(PSBT psbt) {
this.resultType = ResultType.SUCCESS;
this.bbqrType = BBQRType.PSBT;
this.psbt = psbt;
this.transaction = null;
this.strData = null;
this.data = null;
this.error = null;
}
public Result(Transaction transaction) {
this.resultType = ResultType.SUCCESS;
this.bbqrType = BBQRType.TXN;
this.psbt = null;
this.transaction = transaction;
this.strData = null;
this.data = null;
this.error = null;
}
public Result(BBQRType bbqrType, String strData) {
this.resultType = ResultType.SUCCESS;
this.bbqrType = bbqrType;
this.psbt = null;
this.transaction = null;
this.strData = strData;
this.data = null;
this.error = null;
}
public Result(BBQRType bbqrType, byte[] data) {
this.resultType = ResultType.SUCCESS;
this.bbqrType = bbqrType;
this.psbt = null;
this.transaction = null;
this.strData = null;
this.data = data;
this.error = null;
}
public ResultType getResultType() {
return resultType;
}
public BBQRType getBbqrType() {
return bbqrType;
}
public PSBT getPsbt() {
return psbt;
}
public Transaction getTransaction() {
return transaction;
}
public byte[] getData() {
return data;
}
public String toString() {
return strData;
}
public String getError() {
return error;
}
}
public enum ResultType {
SUCCESS, FAILURE;
}
}
@@ -0,0 +1,71 @@
package com.sparrowwallet.sparrow.io.bbqr;
import java.util.ArrayList;
import java.util.List;
public class BBQREncoder {
private final String[] parts;
private int partIndex;
public BBQREncoder(BBQRType bbqrType, BBQREncoding bbqrEncoding, byte[] data, int maxFragmentLength, int firstSeqNum) {
this.parts = encode(bbqrType, bbqrEncoding, data, maxFragmentLength).toArray(new String[0]);
this.partIndex = firstSeqNum;
}
public boolean isSinglePart() {
return parts.length == 1;
}
public String nextPart() {
String currentPart = parts[partIndex];
partIndex++;
if(partIndex > parts.length - 1) {
partIndex = 0;
}
return currentPart;
}
public int getNumParts() {
return parts.length;
}
private List<String> encode(BBQRType type, BBQREncoding desiredEncoding, byte[] data, int desiredChunkSize) {
String encoded;
BBQREncoding encoding = desiredEncoding;
try {
encoded = encoding.encode(data);
if(encoding == BBQREncoding.ZLIB) {
String uncompressed = BBQREncoding.BASE32.encode(data);
if(encoded.length() > uncompressed.length()) {
throw new BBQREncodingException("Compressed data was larger than uncompressed data");
}
}
} catch(BBQREncodingException e) {
encoding = BBQREncoding.BASE32;
encoded = BBQREncoding.BASE32.encode(data);
}
int inputLength = encoded.length();
int numChunks = (inputLength + desiredChunkSize - 1) / desiredChunkSize;
int chunkSize = numChunks == 1 ? desiredChunkSize : (int)Math.ceil((double)inputLength / numChunks);
int modulo = chunkSize % encoding.getPartModulo();
if(modulo > 0) {
chunkSize += (encoding.getPartModulo() - modulo);
}
List<String> chunks = new ArrayList<>();
int startIndex = 0;
for(int i = 0; i < numChunks; i ++) {
int endIndex = Math.min(startIndex + chunkSize, encoded.length());
BBQRHeader bbqrHeader = new BBQRHeader(encoding, type, numChunks, i);
String chunk = bbqrHeader + encoded.substring(startIndex, endIndex);
startIndex = endIndex;
chunks.add(chunk);
}
return chunks;
}
}
@@ -0,0 +1,104 @@
package com.sparrowwallet.sparrow.io.bbqr;
import com.google.common.io.BaseEncoding;
import com.jcraft.jzlib.*;
import com.sparrowwallet.drongo.Utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Locale;
public enum BBQREncoding {
HEX("H") {
@Override
public String encode(byte[] data) throws BBQREncodingException {
return Utils.bytesToHex(data).toUpperCase(Locale.ROOT);
}
@Override
public byte[] decode(String part) throws BBQREncodingException {
return Utils.hexToBytes(part);
}
@Override
public int getPartModulo() {
return 2;
}
}, BASE32("2") {
@Override
public String encode(byte[] data) throws BBQREncodingException {
return BaseEncoding.base32().encode(data).replaceAll("=+$", "");
}
@Override
public byte[] decode(String part) throws BBQREncodingException {
return BaseEncoding.base32().decode(part);
}
@Override
public int getPartModulo() {
return 8;
}
}, ZLIB("Z") {
@Override
public String encode(byte[] data) throws BBQREncodingException {
try {
Deflater deflater = new Deflater(JZlib.Z_BEST_COMPRESSION, 10, true);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DeflaterOutputStream zOut = new DeflaterOutputStream(out, deflater);
zOut.write(data);
zOut.close();
return BASE32.encode(out.toByteArray());
} catch(Exception e) {
throw new BBQREncodingException("Error deflating with zlib", e);
}
}
@Override
public byte[] decode(String part) throws BBQREncodingException {
try {
Inflater inflater = new Inflater(10, true);
ByteArrayInputStream in = new ByteArrayInputStream(BASE32.decode(part));
InflaterInputStream zIn = new InflaterInputStream(in, inflater);
byte[] decoded = zIn.readAllBytes();
zIn.close();
return decoded;
} catch(Exception e) {
throw new BBQREncodingException("Error inflating with zlib", e);
}
}
@Override
public int getPartModulo() {
return 8;
}
};
public static BBQREncoding fromString(String code) {
for(BBQREncoding encoding : values()) {
if(encoding.getCode().equals(code)) {
return encoding;
}
}
throw new IllegalArgumentException("Could not find encoding for code " + code);
}
private final String code;
BBQREncoding(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public abstract String encode(byte[] data) throws BBQREncodingException;
public abstract byte[] decode(String part) throws BBQREncodingException;
public abstract int getPartModulo();
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow.io.bbqr;
public class BBQREncodingException extends BBQRException {
public BBQREncodingException(String message) {
super(message);
}
public BBQREncodingException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow.io.bbqr;
public class BBQRException extends RuntimeException {
public BBQRException(String message) {
super(message);
}
public BBQRException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,40 @@
package com.sparrowwallet.sparrow.io.bbqr;
import java.math.BigInteger;
public record BBQRHeader(BBQREncoding encoding, BBQRType type, int seqTotal, int seqNumber) {
public static final String HEADER = "B$";
public String toString() {
return HEADER + encoding.getCode() + type.getCode() + encodeToBase36(seqTotal) + encodeToBase36(seqNumber);
}
public byte[] decode(String part) {
return encoding.decode(part.substring(8));
}
public static BBQRHeader fromString(String part) {
if(part.length() < 8) {
throw new IllegalArgumentException("Part too short");
}
if(!HEADER.equals(part.substring(0, 2))) {
throw new IllegalArgumentException("Part does not start with " + HEADER);
}
BBQREncoding e = BBQREncoding.fromString(part.substring(2, 3));
BBQRType t = BBQRType.fromString(part.substring(3, 4));
return new BBQRHeader(e, t, decodeFromBase36(part.substring(4, 6)), decodeFromBase36(part.substring(6, 8)));
}
private String encodeToBase36(int number) {
String base36Encoded = new BigInteger(String.valueOf(number)).toString(36);
return String.format("%2s", base36Encoded).replace(' ', '0');
}
private static int decodeFromBase36(String base36) {
BigInteger bigInteger = new BigInteger(base36, 36);
return bigInteger.intValue();
}
}
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.io.bbqr;
public enum BBQRType {
PSBT("P"), TXN("T"), JSON("J"), CBOR("C"), UNICODE("U"), BINARY("B"), EXECUTABLE("X");
private final String code;
BBQRType(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static BBQRType fromString(String code) {
for(BBQRType type : values()) {
if(type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Could not find type for code " + code);
}
}
@@ -15,6 +15,7 @@ public class CardStatus extends CardResponse {
String ver;
BigInteger birth;
boolean tapsigner;
boolean satschip;
List<BigInteger> path;
BigInteger num_backups;
List<BigInteger> slots;
@@ -24,7 +25,7 @@ public class CardStatus extends CardResponse {
boolean testnet;
public boolean isInitialized() {
return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
return ((getCardType() == WalletModel.TAPSIGNER || getCardType() == WalletModel.SATSCHIP) && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
}
public String getIdentifier() {
@@ -42,11 +43,11 @@ public class CardStatus extends CardResponse {
}
public boolean requiresBackup() {
return num_backups == null || num_backups.intValue() == 0;
return !satschip && (num_backups == null || num_backups.intValue() == 0);
}
public WalletModel getCardType() {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
return satschip ? WalletModel.SATSCHIP : (tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD);
}
public int getCurrentSlot() {
@@ -72,6 +73,7 @@ public class CardStatus extends CardResponse {
", ver='" + ver + '\'' +
", birth=" + birth +
", tapsigner=" + tapsigner +
", satschip=" + satschip +
", path=" + path +
", num_backups=" + num_backups +
", slots=" + slots +
@@ -32,10 +32,6 @@ public class CkCardApi extends CardApi {
private final CardProtocol cardProtocol;
private String cvc;
public CkCardApi(String cvc) throws CardException {
this(WalletModel.TAPSIGNER, cvc);
}
public CkCardApi(WalletModel cardType, String cvc) throws CardException {
this.cardType = cardType;
this.cardProtocol = new CardProtocol();
@@ -137,6 +133,8 @@ public class CkCardApi extends CardApi {
public Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty) {
if(cardType == WalletModel.TAPSIGNER) {
return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty);
} else if(cardType == WalletModel.SATSCHIP) {
return new CardImportPane.CardInitializationService(new Satschip(), cvc, entropy, messageProperty);
}
return new CardInitializationService(entropy, messageProperty);
@@ -144,6 +142,10 @@ public class CkCardApi extends CardApi {
@Override
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
if(cardType == WalletModel.SATSCHIP) {
return new CardImportPane.CardImportService(new Satschip(), cvc, derivation, messageProperty);
}
return new CardImportPane.CardImportService(new Tapsigner(), cvc, derivation, messageProperty);
}
@@ -155,11 +157,11 @@ public class CkCardApi extends CardApi {
ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub));
Keystore keystore = new Keystore();
keystore.setLabel(WalletModel.TAPSIGNER.toDisplayString());
keystore.setLabel(cardType.toDisplayString());
keystore.setKeyDerivation(keyDerivation);
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setExtendedPublicKey(derivedXpubkey);
keystore.setWalletModel(WalletModel.TAPSIGNER);
keystore.setWalletModel(cardType);
return keystore;
}
@@ -404,7 +406,7 @@ public class CkCardApi extends CardApi {
@Override
protected PSBT call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
if(cardStatus.getCardType() != WalletModel.TAPSIGNER && cardStatus.getCardType() != WalletModel.SATSCHIP) {
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign transactions.");
}
@@ -465,7 +467,7 @@ public class CkCardApi extends CardApi {
@Override
protected String call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
if(cardStatus.getCardType() != WalletModel.TAPSIGNER && cardStatus.getCardType() != WalletModel.SATSCHIP) {
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign messages.");
}
@@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class Satschip extends Tapsigner {
@Override
public String getKeystoreImportDescription(int account) {
return "Import the keystore from your Satschip by placing it on the card reader.";
}
@Override
public String getName() {
return "Satschip";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SATSCHIP;
}
}
@@ -16,7 +16,7 @@ public class Tapsigner implements KeystoreCardImport {
public boolean isInitialized() throws CardException {
CkCardApi cardApi = null;
try {
cardApi = new CkCardApi(null);
cardApi = new CkCardApi(getWalletModel(), null);
return cardApi.isInitialized();
} finally {
if(cardApi != null) {
@@ -37,7 +37,7 @@ public class Tapsigner implements KeystoreCardImport {
CkCardApi cardApi = null;
try {
cardApi = new CkCardApi(pin);
cardApi = new CkCardApi(getWalletModel(), pin);
CardStatus cardStatus = cardApi.getStatus();
if(cardStatus.isInitialized()) {
throw new IllegalStateException("Card is already initialized.");
@@ -63,7 +63,7 @@ public class Tapsigner implements KeystoreCardImport {
CkCardApi cardApi = null;
try {
cardApi = new CkCardApi(pin);
cardApi = new CkCardApi(getWalletModel(), pin);
CardStatus cardStatus = cardApi.getStatus();
if(!cardStatus.isInitialized()) {
throw new IllegalStateException("Card is not initialized.");
@@ -192,7 +192,7 @@ public class SatoCardApi extends CardApi {
String fullpath = KeyDerivation.writePath(derivation);
cardProtocol.cardVerifyPIN(0, pin);
// 2FA is optionnal, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
// 2FA is optional, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
SatoCardStatus cardStatus = this.getStatus();
if(cardStatus.needs2FA()) {
throw new CardException("Satochip 2FA is not (yet) supported within Sparrow");
@@ -46,7 +46,7 @@ public class SatochipCommandSet {
}
/**
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
* Returns the application info as stored from the last sent SELECT command. Returns null if no successful SELECT
* command has been sent using this command set.
*
* @return the application info object
@@ -347,8 +347,14 @@ public class SatochipCommandSet {
public APDUResponse cardBip32GetExtendedKey(byte[] bytePath) {
byte p1 = (byte) (bytePath.length / 4);
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
return this.cardTransmit(plainApdu);
APDUCommand capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
APDUResponse rapdu = this.cardTransmit(capdu);
if(rapdu.getSw() == 0x9C01) {
// error 0X9C01: no memory available for key derivation => flush memory cache in card
capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x80, bytePath);
rapdu = this.cardTransmit(capdu);
}
return rapdu;
}
/*
@@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.control.CardImportPane;
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.ckcard.Satschip;
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
import com.sparrowwallet.sparrow.io.satochip.Satochip;
import javafx.fxml.FXML;
@@ -25,7 +26,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
public void initializeView() {
List<KeystoreFileImport> fileImporters = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux(), new AirGapVault());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
}
@@ -39,7 +40,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
}
}
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip());
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip(), new Satschip());
for(KeystoreCardImport importer : cardImporters) {
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
@@ -504,19 +504,19 @@ public class ElectrumServer {
}
}
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd) throws ServerException {
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes) throws ServerException {
try {
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
for(int i = indexStart; i < transaction.getOutputs().size() && i < indexEnd; i++) {
TransactionOutput output = transaction.getOutputs().get(i);
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
if(blockTransactionHashes.get(i) == null) {
TransactionOutput output = transaction.getOutputs().get(i);
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
}
}
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactionHashes.add(null);
Map<String, ScriptHashTx[]> result = new HashMap<>();
if(!pathScriptHashes.isEmpty()) {
result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
}
for(String index : result.keySet()) {
@@ -1571,17 +1571,26 @@ public class ElectrumServer {
private final Transaction transaction;
private final int indexStart;
private final int indexEnd;
public TransactionOutputsReferenceService(Transaction transaction) {
this.transaction = transaction;
this.indexStart = 0;
this.indexEnd = transaction.getOutputs().size();
}
private final List<Set<BlockTransactionHash>> blockTransactionHashes;
private final Map<Sha256Hash, BlockTransaction> transactionMap;
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd) {
this.transaction = transaction;
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
this.blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactionHashes.add(null);
}
this.transactionMap = new HashMap<>();
}
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes, Map<Sha256Hash, BlockTransaction> transactionMap) {
this.transaction = transaction;
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
this.blockTransactionHashes = blockTransactionHashes;
this.transactionMap = transactionMap;
}
@Override
@@ -1589,7 +1598,7 @@ public class ElectrumServer {
return new Task<>() {
protected List<BlockTransaction> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd);
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd, blockTransactionHashes);
Set<BlockTransactionHash> setReferences = new HashSet<>();
for(Set<BlockTransactionHash> outputReferences : outputTransactionReferences) {
@@ -1599,16 +1608,16 @@ public class ElectrumServer {
}
setReferences.remove(null);
setReferences.remove(UNFETCHABLE_BLOCK_TRANSACTION);
setReferences.removeIf(ref -> transactionMap.get(ref.getHash()) != null);
List<BlockTransaction> blockTransactions = new ArrayList<>(transaction.getOutputs().size());
for(int i = 0; i < transaction.getOutputs().size(); i++) {
blockTransactions.add(null);
}
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
if(!setReferences.isEmpty()) {
Map<Integer, BlockHeader> blockHeaderMap = electrumServer.getBlockHeaders(null, setReferences);
transactionMap = electrumServer.getTransactions(null, setReferences, blockHeaderMap);
transactionMap.putAll(electrumServer.getTransactions(null, setReferences, blockHeaderMap));
}
for(int i = 0; i < outputTransactionReferences.size(); i++) {
@@ -1693,7 +1702,7 @@ public class ElectrumServer {
for(int i = 0; i < wallets.size(); i++) {
Wallet wallet = wallets.get(i);
updateProgress(i, wallets.size() + StandardAccount.values().length);
updateProgress(i, wallets.size() + StandardAccount.DISCOVERY_ACCOUNTS.size());
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
@@ -1712,7 +1721,7 @@ public class ElectrumServer {
foundAccounts.add(standardAccount);
}
}
updateProgress(i + j, wallets.size() + StandardAccount.values().length);
updateProgress(i + j, wallets.size() + StandardAccount.DISCOVERY_ACCOUNTS.size());
}
for(StandardAccount standardAccount : foundAccounts) {
@@ -1734,7 +1743,7 @@ public class ElectrumServer {
}
List<StandardAccount> accounts = new ArrayList<>();
for(StandardAccount account : StandardAccount.values()) {
for(StandardAccount account : StandardAccount.DISCOVERY_ACCOUNTS) {
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.isWhirlpoolAccount(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
accounts.add(account);
}
@@ -16,14 +16,15 @@ public enum PublicElectrumServer {
DIYNODES_COM("electrum.diynodes.com", "ssl://electrum.diynodes.com:50022", Network.MAINNET),
SETHFORPRIVACY_COM("fulcrum.sethforprivacy.com", "ssl://fulcrum.sethforprivacy.com:50002", Network.MAINNET),
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET),
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET);
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET),
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET);
PublicElectrumServer(String name, String url, Network network) {
this.server = new Server(url, name);
this.network = network;
}
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
private final Server server;
private final Network network;
@@ -117,10 +117,11 @@ public class TcpOverTlsTransport extends TcpTransport {
}
protected boolean shouldSaveCertificate() {
//Avoid saving the certificates for blockstream.info public servers - they change too often and encourage approval complacency
if(PublicElectrumServer.BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())
|| PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())) {
return false;
//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 false;
}
}
return Storage.getCertificateFile(server.getHost()) == null;
@@ -20,12 +20,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class);
private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version";
private static String version;
@Override
protected Task<VersionUpdatedEvent> createTask() {
return new Task<>() {
protected VersionUpdatedEvent call() {
try {
VersionCheck versionCheck = getVersionCheck();
version = versionCheck.version;
if(isNewer(versionCheck) && verifySignature(versionCheck)) {
return new VersionUpdatedEvent(versionCheck.version);
}
@@ -91,9 +94,12 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
return false;
}
public static String getVersion() {
return version;
}
private static class VersionCheck {
public String version;
public Map<String, String> signatures;
}
}
@@ -46,6 +46,8 @@ public class BitcoindClient {
private static final int DEFAULT_GAP_LIMIT = 1000;
private static final int POSTMIX_GAP_LIMIT = 4000;
private static final long PRUNED_RESCAN_TIMEGAP_MILLIS = 7200*1000;
private final JsonRpcClient jsonRpcClient;
private final Timer timer = new Timer(true);
private final Store store = new Store();
@@ -258,7 +260,7 @@ public class BitcoindClient {
}
private ScanDate getScanDate(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose, Date earliestBirthDate) {
Integer range = (keyPurpose == null ? null : getHighestUsedIndex(normalizedDescriptor, wallet, keyPurpose) + getGapLimit(wallet, keyPurpose));
Integer range = (keyPurpose == null ? null : Math.max(getWalletRange(normalizedDescriptor, wallet, keyPurpose), getDefaultRange(wallet, keyPurpose)));
//Force a rescan if loading a wallet with a birthday later than existing transactions, or if the wallet birthdate has been set or changed to an earlier date from the last check
boolean forceRescan = false;
@@ -272,13 +274,20 @@ public class BitcoindClient {
return new ScanDate(earliestBirthDate, range, forceRescan);
}
private int getWalletRange(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose) {
int maxIndex = getHighestUsedIndex(normalizedDescriptor, wallet, keyPurpose) + wallet.getGapLimit();
//Default keypool size of 1000 will mean no rescans with a normal (<1000) gap limit as range is automatically extended by keypool size on address use
//Rounding up to the nearest 500 will ensure reduced rescans where gap limit is over 1000
return maxIndex % 500 == 0 ? maxIndex : ((maxIndex / 500) + 1) * 500;
}
private int getHighestUsedIndex(String descriptor, Wallet wallet, KeyPurpose keyPurpose) {
int highestUsedIndex = wallet.getFreshNode(keyPurpose).getIndex() - 1;
return descriptorUsedIndexes.compute(descriptor, (d, lastHighestUsedIndex) -> lastHighestUsedIndex == null ? highestUsedIndex : Math.max(highestUsedIndex, lastHighestUsedIndex));
}
private int getGapLimit(Wallet wallet, KeyPurpose keyPurpose) {
return Math.max(wallet.getGapLimit(), wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && keyPurpose == KeyPurpose.RECEIVE ? POSTMIX_GAP_LIMIT : DEFAULT_GAP_LIMIT);
private int getDefaultRange(Wallet wallet, KeyPurpose keyPurpose) {
return wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && keyPurpose == KeyPurpose.RECEIVE ? POSTMIX_GAP_LIMIT : DEFAULT_GAP_LIMIT;
}
private void importDescriptors(Map<String, ScanDate> descriptors) throws ScanDateBeforePruneException {
@@ -320,6 +329,7 @@ public class BitcoindClient {
}
}
Optional<Date> optPrunedDate = pruned ? getPrunedDate() : Optional.empty();
Map<String, ScanDate> importingDescriptors = new LinkedHashMap<>(descriptors);
importingDescriptors.keySet().removeAll(importedDescriptors.keySet());
for(Map.Entry<String, ScanDate> entry : descriptors.entrySet()) {
@@ -331,6 +341,8 @@ public class BitcoindClient {
ScanDate importedScanDate = importedDescriptors.get(entry.getKey());
if(scanDate.range != null && importedScanDate != null && importedScanDate.range != null && scanDate.range > importedScanDate.range) {
Date rescanSince = scanDate.rescanSince != null && (importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince)) ? scanDate.rescanSince : importedScanDate.rescanSince;
Optional<Date> optPrunedStart = optPrunedDate.map(date -> new Date(date.getTime() + PRUNED_RESCAN_TIMEGAP_MILLIS));
rescanSince = optPrunedStart.isPresent() && (rescanSince == null || optPrunedStart.get().after(rescanSince)) ? optPrunedStart.get() : rescanSince;
importingDescriptors.put(entry.getKey(), new ScanDate(rescanSince, scanDate.range, false));
} else if(scanDate.forceRescan) {
if(scanDate.rescanSince != null && (importedScanDate == null || importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince))) {
@@ -339,15 +351,12 @@ public class BitcoindClient {
}
}
if(pruned) {
Optional<Date> optPrunedDate = getPrunedDate();
if(optPrunedDate.isPresent()) {
Date prunedDate = optPrunedDate.get();
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
if(optPrePruneImport.isPresent()) {
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
}
if(optPrunedDate.isPresent()) {
Date prunedDate = optPrunedDate.get();
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
if(optPrePruneImport.isPresent()) {
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
}
}
@@ -424,7 +433,7 @@ public class BitcoindClient {
}
try {
if(listTransaction.category() == Category.receive) {
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
//Transactions received to an address can be added directly
Address address = Address.fromString(listTransaction.address());
String updatedScriptHash = store.addAddressTransaction(address, listTransaction);
@@ -19,7 +19,7 @@ public class Store {
private final Map<String, MempoolEntry> mempoolEntries = new HashMap<>();
public String addAddressTransaction(Address address, ListTransaction listTransaction) {
if(listTransaction.category() == Category.receive) {
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
fundingAddresses.put(new HashIndex(Sha256Hash.wrap(listTransaction.txid()), listTransaction.vout()), address);
}
@@ -49,7 +49,7 @@ public class TxEntry implements Comparable<TxEntry> {
return height - o.height;
}
if(height == 0) {
if(height <= 0) {
return tx_hash.compareTo(o.tx_hash);
}
@@ -175,6 +175,7 @@ public class GeneralPreferencesController extends PreferencesDetailController {
validateDerivationPaths.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
config.setValidateDerivationPaths(newValue);
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!newValue));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!newValue));
});
groupByAddress.setSelected(config.isGroupByAddress());
@@ -0,0 +1,77 @@
package com.sparrowwallet.sparrow.terminal;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import java.util.Collections;
public class PassphraseDialog extends DialogWindow {
private final Label masterFingerprint;
private final TextBox passphrase;
private String result;
public PassphraseDialog(String walletName, Keystore keystore) {
super("Passphrase for " + walletName);
setHints(Collections.singleton(Window.Hint.CENTERED));
this.masterFingerprint = new Label("");
this.passphrase = new TextBox();
this.passphrase.setMask('*');
Panel buttonPanel = new Panel();
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
buttonPanel.addComponent(new Button(LocalizedString.OK.toString(), this::onOK).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
buttonPanel.addComponent(new Button(LocalizedString.Cancel.toString(), this::onCancel));
Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new GridLayout(1).setLeftMarginSize(1).setRightMarginSize(1));
mainPanel.addComponent(new Label("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel()));
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
passphrase.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false)).addTo(mainPanel);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
mainPanel.addComponent(masterFingerprint);
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false)).addTo(mainPanel);
setComponent(mainPanel);
passphrase.setTextChangeListener((newText, changedByUserInteraction) -> {
setMasterFingerprintLabel(keystore, newText);
});
setMasterFingerprintLabel(keystore, "");
}
private void onOK() {
result = passphrase.getText();
close();
}
private void onCancel() {
close();
}
@Override
public String showDialog(WindowBasedTextGUI textGUI) {
result = null;
super.showDialog(textGUI);
return result;
}
private void setMasterFingerprintLabel(Keystore keystore, String passphrase) {
masterFingerprint.setText("Master fingerprint: " + Utils.bytesToHex(getMasterFingerprint(keystore, passphrase)));
}
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
try {
Keystore copyKeystore = keystore.copy();
copyKeystore.getSeed().setPassphrase(passphrase);
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
} catch(MnemonicException e) {
throw new RuntimeException(e);
}
}
}
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.terminal;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder;
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.InteractionServices;
import javafx.application.Platform;
@@ -103,10 +102,8 @@ public class TerminalInteractionServices implements InteractionServices {
}
private Optional<String> showPassphraseDialog(String walletName, Keystore keystore) {
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Passphrase for " + walletName);
builder.setDescription("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel());
builder.setPasswordInput(true);
String passphrase = builder.build().showDialog(SparrowTerminal.get().getGui());
PassphraseDialog passphraseDialog = new PassphraseDialog(walletName, keystore);
String passphrase = passphraseDialog.showDialog(SparrowTerminal.get().getGui());
return passphrase == null ? Optional.empty() : Optional.of(passphrase);
}
}
@@ -11,6 +11,8 @@ import java.util.ArrayList;
import java.util.List;
final class AddAccountDialog extends DialogWindow {
private static final int MAX_SHOWN_ACCOUNTS = 8;
private ComboBox<DisplayStandardAccount> standardAccounts;
private StandardAccount standardAccount;
@@ -40,7 +42,7 @@ final class AddAccountDialog extends DialogWindow {
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount)) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount) && availableAccounts.size() <= MAX_SHOWN_ACCOUNTS) {
availableAccounts.add(standardAccount);
}
}
@@ -121,6 +121,7 @@ public class LoadWallet implements Runnable {
}
} catch(Exception e) {
log.error("Wallet Error", e);
SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(loadingDialog));
showErrorDialog("Wallet Error", e.getMessage());
} finally {
walletAndKey.clear();
@@ -6,9 +6,9 @@ import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.table.Table;
import com.googlecode.lanterna.gui2.table.TableModel;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.terminal.wallet.table.CoinTableCell;
@@ -94,7 +94,7 @@ public class TransactionsDialog extends WalletDialog {
balance.setText(formatBitcoinValue(walletTransactionsEntry.getBalance(), true));
mempoolBalance.setText(formatBitcoinValue(walletTransactionsEntry.getMempoolBalance(), true));
if(AppServices.getFiatCurrencyExchangeRate() != null) {
if(AppServices.getFiatCurrencyExchangeRate() != null && Config.get().getExchangeSource() != ExchangeSource.NONE) {
fiatBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate())));
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate())));
} else {
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.terminal.ModalDialog;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
@@ -233,7 +234,7 @@ public class UtxosDialog extends WalletDialog {
balance.setText(formatBitcoinValue(walletUtxosEntry.getBalance(), true));
mempoolBalance.setText(formatBitcoinValue(walletUtxosEntry.getMempoolBalance(), true));
if(AppServices.getFiatCurrencyExchangeRate() != null) {
if(AppServices.getFiatCurrencyExchangeRate() != null && Config.get().getExchangeSource() != ExchangeSource.NONE) {
fiatBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate())));
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate())));
} else {
@@ -19,6 +19,8 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.payjoin.Payjoin;
@@ -54,6 +56,7 @@ import tornadofx.control.Fieldset;
import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form;
import javax.swing.text.html.Option;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
@@ -437,8 +440,11 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt);
}
headersForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> {
transactionDiagram.update(walletTransaction);
});
transactionDiagram.labelProperty().set(transactionDiagramLabel);
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
@@ -671,7 +677,8 @@ public class HeadersController extends TransactionFormController implements Init
List<Payment> payments = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], null, txOutput.getValue(), false));
BlockTransactionHashIndex receivedTxo = getBlockTransactionOutput(txOutput);
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : null, txOutput.getValue(), false));
} catch(Exception e) {
//ignore
}
@@ -705,6 +712,17 @@ public class HeadersController extends TransactionFormController implements Init
return null;
}
private BlockTransactionHashIndex getBlockTransactionOutput(TransactionOutput txOutput) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
Optional<BlockTransactionHashIndex> output = openWallet.getWalletTxos().keySet().stream().filter(ref -> ref.getHash().equals(txOutput.getHash()) && ref.getIndex() == txOutput.getIndex()).findFirst();
if(output.isPresent()) {
return output.get();
}
}
return null;
}
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
signaturesForm.setVisible(false);
blockchainForm.setVisible(true);
@@ -886,16 +904,21 @@ public class HeadersController extends TransactionFormController implements Init
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
//TODO: Remove once Cobo Vault has upgraded to UR2.0
//TODO: Remove once Cobo Vault support has been removed
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getSource().equals(KeystoreSource.SW_SEED));
boolean selectBbqrOption = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD));
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning
boolean includeNonWitnessUtxos = !Arrays.asList(ScriptType.WITNESS_TYPES).contains(headersForm.getSigningWallet().getScriptType());
CryptoPSBT cryptoPSBT = new CryptoPSBT(headersForm.getPsbt().serialize(true, includeNonWitnessUtxos));
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), addLegacyEncodingOption, true);
byte[] psbtBytes = headersForm.getPsbt().serialize(true, includeNonWitnessUtxos);
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.PSBT, psbtBytes) : null;
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, addLegacyEncodingOption, true, selectBbqrOption);
qrDisplayDialog.initOwner(toggleButton.getScene().getWindow());
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
scanPSBT(event);
}
}
@@ -1334,7 +1357,7 @@ public class HeadersController extends TransactionFormController implements Init
if(headersForm.getInputTransactions() != null) {
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
}
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
headersForm.setWalletTransaction(getWalletTransaction(allFetchedInputTransactions));
}
}
}
@@ -1500,7 +1523,7 @@ public class HeadersController extends TransactionFormController implements Init
updateType();
updateSize();
updateFee(headersForm.getPsbt().getFee());
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
}
}
@@ -1564,7 +1587,7 @@ public class HeadersController extends TransactionFormController implements Init
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? (walletNode.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)"));
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
}
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null && headersForm.getName() != null) { //The norm - sending out
output.getSpentBy().setLabel(headersForm.getName() + " (input)");
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output.getSpentBy(), HashIndexEntry.Type.INPUT, walletNode.getKeyPurpose()));
}
@@ -1597,7 +1620,7 @@ public class HeadersController extends TransactionFormController implements Init
public void psbtReordered(PSBTReorderedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt())) {
updateTxId();
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
headersForm.setWalletTransaction(getWalletTransaction(headersForm.getInputTransactions()));
}
}
@@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@@ -124,7 +125,7 @@ public class InputController extends TransactionFormController implements Initia
inputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
updateInputLegendFromWallet(txInput, signingWallet);
});
updateInputLegendFromWallet(txInput, inputForm.getSigningWallet());
updateInputLegendFromWallet(txInput, inputForm.getWallet());
initializeInputFields(txInput, psbtInput);
initializeScriptFields(txInput, psbtInput);
@@ -142,19 +143,19 @@ public class InputController extends TransactionFormController implements Initia
return "Input #" + txInput.getIndex();
}
private void updateInputLegendFromWallet(TransactionInput txInput, Wallet signingWallet) {
private void updateInputLegendFromWallet(TransactionInput txInput, Wallet wallet) {
String baseText = getLegendText(txInput);
if(signingWallet != null) {
if(wallet != null) {
if(inputForm.isWalletTxo()) {
inputFieldset.setText(baseText + " from " + signingWallet.getFullDisplayName());
inputFieldset.setIcon(TransactionDiagram.getTxoGlyph());
inputFieldset.setText(baseText + " from " + wallet.getFullDisplayName());
inputFieldset.setIcon(GlyphUtils.getTxoGlyph());
} else {
inputFieldset.setText(baseText + " - External");
inputFieldset.setIcon(TransactionDiagram.getMixGlyph());
inputFieldset.setText(baseText + (txInput.isCoinBase() ? " - Coinbase" : " - External"));
inputFieldset.setIcon(GlyphUtils.getMixGlyph());
}
} else {
inputFieldset.setText(baseText);
inputFieldset.setIcon(null);
inputFieldset.setText(baseText + (txInput.isCoinBase() ? " - Coinbase" : " - External"));
inputFieldset.setIcon(GlyphUtils.getExternalInputGlyph());
}
}
@@ -1,14 +1,20 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutPoint;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Label;
import org.controlsfx.glyphfont.Glyph;
import java.io.IOException;
import java.util.Optional;
public class InputForm extends IndexedTransactionForm {
public InputForm(TransactionData txdata, PSBTInput psbtInput) {
@@ -48,7 +54,7 @@ public class InputForm extends IndexedTransactionForm {
public boolean isWalletTxo() {
TransactionInput txInput = getTransactionInput();
return getSigningWallet() != null && getSigningWallet().isWalletTxo(txInput);
return getWallet() != null && getWallet().isWalletTxo(txInput);
}
@Override
@@ -67,6 +73,28 @@ public class InputForm extends IndexedTransactionForm {
}
public String toString() {
return "Input #" + getIndex();
if(getTransactionInput().isCoinBase()) {
return "Coinbase";
}
TransactionOutPoint outPoint = getTransactionInput().getOutpoint();
return outPoint.getHash().toString().substring(0, 8) + "..:" + outPoint.getIndex();
}
@Override
public Label getLabel() {
if(getWalletTransaction() != null) {
TransactionOutPoint outPoint = getTransactionInput().getOutpoint();
Optional<BlockTransactionHashIndex> optRef = getWalletTransaction().getSelectedUtxos().keySet().stream()
.filter(txo -> txo.getHash().equals(outPoint.getHash()) && txo.getIndex() == outPoint.getIndex()).findFirst();
Glyph inputGlyph = isWalletTxo() ? GlyphUtils.getTxoGlyph() : (getWallet() != null ? GlyphUtils.getMixGlyph() : GlyphUtils.getExternalInputGlyph());
if(optRef.isPresent() && optRef.get().getLabel() != null) {
return new Label(optRef.get().getLabel(), inputGlyph);
} else {
return new Label(toString(), inputGlyph);
}
}
return super.getLabel();
}
}
@@ -5,8 +5,8 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.PSBTReorderedEvent;
@@ -66,7 +66,10 @@ public class OutputController extends TransactionFormController implements Initi
outputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
updateOutputLegendFromWallet(txOutput, signingWallet);
});
updateOutputLegendFromWallet(txOutput, outputForm.getSigningWallet());
outputForm.walletTransactionProperty().addListener((observable, oldValue, walletTransaction) -> {
updateOutputLegendFromWallet(txOutput, walletTransaction != null ? walletTransaction.getWallet() : null);
});
updateOutputLegendFromWallet(txOutput, outputForm.getWallet());
value.setValue(txOutput.getValue());
to.setVisible(false);
@@ -103,23 +106,40 @@ public class OutputController extends TransactionFormController implements Initi
return "Output #" + txOutput.getIndex();
}
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet signingWallet) {
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet wallet) {
String baseText = getLegendText(txOutput);
if(signingWallet != null) {
WalletTransaction walletTx = outputForm.getWalletTransaction();
if(walletTx != null) {
List<WalletTransaction.Output> outputs = walletTx.getOutputs();
if(outputForm.getIndex() < outputs.size()) {
WalletTransaction.Output output = outputs.get(outputForm.getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
outputFieldset.setText(baseText);
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
outputFieldset.setText(baseText + (toWallet == null ? (toNode != null ? " - Consolidation" : " - Payment") : " - Received to " + toWallet.getFullDisplayName()));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputFieldset.setText(baseText + " - Change to " + changeOutput.getWalletNode().toString());
} else {
outputFieldset.setText(baseText);
}
} else {
outputFieldset.setText(baseText);
}
} else if(wallet != null) {
if(outputForm.isWalletChange()) {
outputFieldset.setText(baseText + " - Change");
outputFieldset.setIcon(TransactionDiagram.getChangeGlyph());
} else if(outputForm.isWalletConsolidation()) {
outputFieldset.setText(baseText + " - Consolidation");
outputFieldset.setIcon(TransactionDiagram.getConsolidationGlyph());
} else {
outputFieldset.setText(baseText + " - Payment");
outputFieldset.setIcon(TransactionDiagram.getPaymentGlyph());
}
} else {
outputFieldset.setText(baseText);
outputFieldset.setIcon(null);
}
outputFieldset.setIcon(outputForm.getLabel().getGraphic());
}
private void updateSpent(List<BlockTransaction> outputTransactions) {
@@ -1,12 +1,20 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.Label;
import java.io.IOException;
import java.util.*;
public class OutputForm extends IndexedTransactionForm {
public OutputForm(TransactionData txdata, PSBTOutput psbtOutput) {
@@ -26,15 +34,15 @@ public class OutputForm extends IndexedTransactionForm {
}
public boolean isWalletConsolidation() {
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(KeyPurpose.RECEIVE).containsKey(getTransactionOutput().getScript()));
return (getWallet() != null && getWallet().getWalletOutputScripts(KeyPurpose.RECEIVE).containsKey(getTransactionOutput().getScript()));
}
public boolean isWalletChange() {
return (getSigningWallet() != null && getSigningWallet().getWalletOutputScripts(getSigningWallet().getChangeKeyPurpose()).containsKey(getTransactionOutput().getScript()));
return (getWallet() != null && getWallet().getWalletOutputScripts(getWallet().getChangeKeyPurpose()).containsKey(getTransactionOutput().getScript()));
}
public boolean isWalletPayment() {
return getSigningWallet() != null;
return getWallet() != null;
}
@Override
@@ -53,6 +61,33 @@ public class OutputForm extends IndexedTransactionForm {
}
public String toString() {
return "Output #" + getIndex();
Address address = getTransactionOutput().getScript().getToAddress();
return address != null ? address.toString() : "Output #" + getIndex();
}
@Override
public Label getLabel() {
if(getWalletTransaction() != null) {
List<WalletTransaction.Output> outputs = getWalletTransaction().getOutputs();
if(getIndex() < outputs.size()) {
WalletTransaction.Output output = outputs.get(getIndex());
if(output instanceof WalletTransaction.NonAddressOutput) {
List<ScriptChunk> chunks = output.getTransactionOutput().getScript().getChunks();
if(!chunks.isEmpty() && chunks.get(0).isOpCode() && chunks.get(0).getOpcode() == ScriptOpCodes.OP_RETURN) {
return new Label(chunks.get(0).toString(), GlyphUtils.getOpcodeGlyph());
} else {
return new Label("Output #" + getIndex(), GlyphUtils.getOpcodeGlyph());
}
} else if(output instanceof WalletTransaction.PaymentOutput paymentOutput) {
Payment payment = paymentOutput.getPayment();
return new Label(payment.getLabel() != null && payment.getType() != Payment.Type.FAKE_MIX && payment.getType() != Payment.Type.MIX ? payment.getLabel() : payment.getAddress().toString(),
GlyphUtils.getOutputGlyph(getWalletTransaction(), payment));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
return new Label("Change", GlyphUtils.getChangeGlyph());
}
}
}
return super.getLabel();
}
}
@@ -5,12 +5,10 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
import com.sparrowwallet.sparrow.control.TransactionHexArea;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
@@ -20,6 +18,7 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
@@ -33,6 +32,7 @@ import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TransactionController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(TransactionController.class);
@@ -158,28 +158,11 @@ public class TransactionController implements Initializable {
setContextMenu(null);
if(form != null) {
setText(form.toString());
Label label = form.getLabel();
label.setMaxWidth(115);
setGraphic(label);
if(form.getSigningWallet() != null) {
if(form instanceof InputForm) {
InputForm inputForm = (InputForm)form;
if(inputForm.isWalletTxo()) {
setGraphic(TransactionDiagram.getTxoGlyph());
} else {
setGraphic(TransactionDiagram.getMixGlyph());
}
}
if(form instanceof OutputForm) {
OutputForm outputForm = (OutputForm)form;
if(outputForm.isWalletChange()) {
setGraphic(TransactionDiagram.getChangeGlyph());
} else if(outputForm.isWalletConsolidation()) {
setGraphic(TransactionDiagram.getConsolidationGlyph());
} else {
setGraphic(TransactionDiagram.getPaymentGlyph());
}
}
setOnDragDetected(null);
setOnDragOver(null);
setOnDragDropped(null);
@@ -196,6 +179,10 @@ public class TransactionController implements Initializable {
txtree.refresh();
});
txdata.walletTransactionProperty().addListener((observable, oldValue, newValue) -> {
txtree.refresh();
});
txtree.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedItem) -> {
TransactionForm transactionForm = selectedItem.getValue();
if(transactionForm instanceof PageForm) {
@@ -439,7 +426,39 @@ public class TransactionController implements Initializable {
private void fetchOutputBlockTransactions(int indexStart, int indexEnd) {
if(AppServices.isConnected() && getPSBT() == null && indexStart < getTransaction().getOutputs().size()) {
int maxIndex = Math.min(getTransaction().getOutputs().size(), indexEnd);
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex);
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(getTransaction().getOutputs().size());
for(int i = 0; i < getTransaction().getOutputs().size(); i++) {
blockTransactionHashes.add(null);
}
Map<Script, WalletNode> openWalletOutputScripts = new HashMap<>();
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).collect(Collectors.toList())) {
openWalletOutputScripts.putAll(wallet.getWalletOutputScripts());
}
for(int i = indexStart; i < getTransaction().getOutputs().size() && i < maxIndex; i++) {
TransactionOutput output = getTransaction().getOutputs().get(i);
List<ScriptChunk> chunks = output.getScript().getChunks();
if(!chunks.isEmpty() && chunks.get(0).isOpCode() && chunks.get(0).getOpcode() == ScriptOpCodes.OP_RETURN) {
blockTransactionHashes.set(i, Collections.emptySet());
} else if(openWalletOutputScripts.get(output.getScript()) != null) {
WalletNode walletNode = openWalletOutputScripts.get(output.getScript());
if(walletNode != null) {
Set<BlockTransactionHash> references = walletNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo.getSpentBy()) : Stream.empty()).collect(Collectors.toCollection(TreeSet::new));
blockTransactionHashes.set(i, references);
for(BlockTransactionHash reference : references) {
BlockTransaction blkTx = walletNode.getWallet().getWalletTransaction(reference.getHash());
if(blkTx != null) {
transactionMap.put(reference.getHash(), blkTx);
}
}
}
}
}
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(getTransaction(), indexStart, maxIndex, blockTransactionHashes, transactionMap);
transactionOutputsReferenceService.setOnSucceeded(successEvent -> {
List<BlockTransaction> outputTransactions = transactionOutputsReferenceService.getValue();
Platform.runLater(() -> {
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
@@ -30,6 +27,7 @@ public class TransactionData {
private final ObservableMap<Wallet, Storage> availableWallets = FXCollections.observableHashMap();
private final SimpleObjectProperty<Wallet> signingWallet = new SimpleObjectProperty<>(this, "signingWallet", null);
private final ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>());
private final SimpleObjectProperty<WalletTransaction> walletTransaction = new SimpleObjectProperty<>(this, "walletTransaction", null);
public TransactionData(String name, PSBT psbt) {
this(name, psbt.getTransaction());
@@ -179,4 +177,20 @@ public class TransactionData {
return signingWalletNodes;
}
public WalletTransaction getWalletTransaction() {
return walletTransaction.get();
}
public SimpleObjectProperty<WalletTransaction> walletTransactionProperty() {
return walletTransaction;
}
public void setWalletTransaction(WalletTransaction walletTransaction) {
this.walletTransaction.set(walletTransaction);
}
public Wallet getWallet() {
return getSigningWallet() != null ? getSigningWallet() : (getWalletTransaction() != null ? getWalletTransaction().getWallet() : null);
}
}
@@ -4,14 +4,12 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableMap;
import javafx.scene.Node;
import javafx.scene.control.Label;
import java.io.IOException;
import java.util.Collection;
@@ -98,6 +96,22 @@ public abstract class TransactionForm {
return txdata.getSigningWalletNodes();
}
public WalletTransaction getWalletTransaction() {
return txdata.getWalletTransaction();
}
public SimpleObjectProperty<WalletTransaction> walletTransactionProperty() {
return txdata.walletTransactionProperty();
}
public void setWalletTransaction(WalletTransaction walletTransaction) {
txdata.setWalletTransaction(walletTransaction);
}
public Wallet getWallet() {
return txdata.getWallet();
}
public boolean isEditable() {
if(getBlockTransaction() != null) {
return false;
@@ -120,4 +134,8 @@ public abstract class TransactionForm {
public abstract Node getContents() throws IOException;
public abstract TransactionView getView();
public Label getLabel() {
return new Label(toString());
}
}
@@ -163,7 +163,7 @@ public class AddressesController extends WalletFormController implements Initial
writer.write(getDerivationPath(indexNode));
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();
writer.write(optLabelEntry.isPresent() ? optLabelEntry.get().getLabel() : "");
writer.write(optLabelEntry.isPresent() ? optLabelEntry.get().getLabel() : indexNode.getLabel());
writer.endRecord();
}
writer.close();
@@ -186,7 +186,7 @@ public class KeystoreController extends WalletFormController implements Initiali
return null;
}));
derivation.textProperty().addListener((observable, oldValue, newValue) -> {
if(KeyDerivation.isValid(newValue) && !walletForm.getWallet().derivationMatchesAnotherScriptType(newValue)) {
if(KeyDerivation.isValid(newValue) && !walletForm.getWallet().derivationMatchesAnotherScriptType(newValue) && !walletForm.getWallet().derivationMatchesAnotherNetwork(newValue)) {
keystore.setKeyDerivation(new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), newValue));
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_DERIVATION));
}
@@ -287,7 +287,8 @@ public class KeystoreController extends WalletFormController implements Initiali
validationSupport.registerValidator(derivation, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation is invalid", !KeyDerivation.isValid(newValue)),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation matches another script type", walletForm.getWallet().derivationMatchesAnotherScriptType(newValue))
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation matches another script type", walletForm.getWallet().derivationMatchesAnotherScriptType(newValue)),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation matches another network", walletForm.getWallet().derivationMatchesAnotherNetwork(newValue))
));
validationSupport.registerValidator(fingerprint, Validator.combine(
@@ -493,7 +494,7 @@ public class KeystoreController extends WalletFormController implements Initiali
log.error("Error communicating with card", e);
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
});
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authentication delay to clear...", "/image/tapsigner.png", authDelayService);
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authentication delay to clear...", "/image/" + cardApi.getCardType().getType() + ".png", authDelayService);
serviceProgressDialog.initOwner(cardServiceButtons.getScene().getWindow());
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
authDelayService.start();

Some files were not shown because too many files have changed in this diff Show More