Compare commits

...
139 Commits
Author SHA1 Message Date
Craig Raw ebd629db3a v1.5.1 2021-10-07 13:11:28 +02:00
Craig Raw c18a2f4388 improve tor identity management 2021-10-07 12:23:28 +02:00
Craig Raw 6f95dbe309 support bitcoin core connections over tor 2021-10-06 20:37:58 +02:00
Craig Raw 576253e651 fix remaining issue when utxo date is null 2021-10-06 10:08:56 +02:00
Craig Raw b9d6cb17d4 indicate when entered seed is of unsupported electrum type 2021-10-06 08:23:08 +02:00
Craig Raw 3b730a1711 preserve check menu item state across windows 2021-10-05 16:38:56 +02:00
Craig Raw 8c0a1932cf add prevent computer sleep functionality 2021-10-05 16:16:32 +02:00
Craig Raw 35b57f9d69 add txid to transactions csv download 2021-10-05 10:55:20 +02:00
Craig Raw d6ad7f4808 handle null dates in date cell 2021-10-05 10:39:11 +02:00
Craig Raw 3e47a49f49 core connection: default to cookie authentication when user/pass are not filled in 2021-10-05 10:08:51 +02:00
Craig Raw 4c817d243d fix long specter desktop keystore labels 2021-10-04 14:45:02 +02:00
Craig Raw 22b7b659f3 include amount in transaction pie chart tooltip 2021-10-04 14:44:19 +02:00
Craig Raw 9dd6068e69 make temp backup permanent when created in a previous process 2021-10-04 13:49:41 +02:00
craigrawandGitHub 54baee57e1 Merge pull request #235 from romanz/master
Allow running `sparrow` script from other paths
2021-10-04 12:54:48 +02:00
Roman Zeyde 581aaf288e Allow running sparrow script from other paths
IIUC, it currently assumes that it's run locally using `./sparrow`.
2021-10-04 13:44:42 +03:00
Craig Raw 2fa47e640d various whirlpool-related improvements 2021-10-04 12:16:03 +02:00
Craig Raw ea03dece72 add lock wallet functionality 2021-10-01 15:47:01 +02:00
Craig Raw 8e0b9a3ea0 ask for passphrase re-entry when creating a bip39 wallet 2021-09-30 12:49:03 +02:00
Craig Raw 67179127e3 minor whirlpool related fixes 2021-09-30 10:52:40 +02:00
Craig Raw 4ebee8a8f3 dont allow double click to receive on whirlpool wallet 2021-09-29 10:41:16 +02:00
Craig Raw 2548e77d90 followup to disable adding accounts to p2sh legacy multisig 2021-09-29 10:32:43 +02:00
Craig Raw 58e3b9dcdd add multiple account functionality 2021-09-29 10:11:51 +02:00
Craig Raw 429b733140 prevent setting separate passwords on child wallets 2021-09-27 13:24:29 +02:00
Craig Raw 56e3a54ae0 clarify and improve wallet password and keystore passphrase entry 2021-09-27 12:49:46 +02:00
Craig Raw a934ffa76c fix issues when removing selected items from utxotreetable 2021-09-27 11:46:50 +02:00
Craig Raw 86a49e0d9a fix npe showing the mix to dialog when non-standard wallets are loaded 2021-09-27 10:08:48 +02:00
Craig Raw 761e9c9b7e use wallet unit when displaying pool denomination 2021-09-27 09:33:41 +02:00
Craig Raw 712241873f show reason for mix error in tooltip 2021-09-27 08:54:42 +02:00
Craig Raw 31f9cca33a handle electrs batching detection better 2021-09-27 08:15:20 +02:00
Craig Raw 395e90e2a5 undo #181: always create bitcoin core wallet if missing 2021-09-27 08:14:35 +02:00
Craig Raw b2657cdcfb v1.5.0 final 2021-09-23 15:52:33 +02:00
Craig Raw 6bbae204a6 restrict mixing to mainnet and testnet 2021-09-23 15:30:13 +02:00
Craig Raw 0b55dd8a1e ui fixes to mix start and stop 2021-09-23 14:18:37 +02:00
Craig Raw f74287697c use master wallet passphrase for child wallets when loading 2021-09-23 12:49:06 +02:00
Craig Raw 7a3e1dfa1f change default log level to warn 2021-09-23 12:28:47 +02:00
Craig Raw 427a6925ee mainnet mixing, improve pools selection, other fixes 2021-09-23 10:59:40 +02:00
Craig Raw c55b19af0f tor status windows visual fixes 2021-09-21 11:55:23 +02:00
Craig Raw e12f7a634a add tor status indicator to status bar 2021-09-21 11:22:09 +02:00
Craig Raw ca1f934138 update README for https cloning 2021-09-20 16:42:55 +02:00
Craig Raw 4f9b87b74e fix recursive clone via https 2021-09-20 16:37:03 +02:00
Craig Raw e1e5df78c6 fix text truncation in whirlpool dialog on windows 2021-09-20 15:51:57 +02:00
Craig Raw cfd06a8513 use cached tx0previews, only save mixconfig on apply 2021-09-20 15:30:13 +02:00
craigrawandGitHub 1c1099217b Merge pull request #198 from zeroleak/whirlpool-client-0.23.30-early5
update to whirlpool-client 0.23.31
2021-09-20 13:10:47 +02:00
zeroleak 6939d8a06a upgrade to whirlpool-client 0.23.33 2021-09-18 11:41:52 +02:00
zeroleak dbebade3ab update to whirlpool-client 0.23.31 2021-09-11 08:17:15 +02:00
Craig Raw 5895837b60 hide receive tab on whirlpool wallets 2021-09-10 15:35:40 +02:00
Craig Raw 0f9e9d9c35 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-09-10 13:58:47 +02:00
Craig Raw 59452e64ea add additional required packages 2021-09-10 13:58:18 +02:00
craigrawandGitHub 573d5b2c18 Merge pull request #195 from RequestPrivacy/patch-1
Fix skdman link
2021-09-10 10:01:47 +02:00
Craig Raw 2f0dc5bea8 ensure submodule is also on correct tag 2021-09-10 09:41:30 +02:00
RequestPrivacyandGitHub 27f7d5ad38 Fix skdman link 2021-09-09 16:16:54 +00:00
Craig Raw 414ffc2648 add first draft of reproducible build documentation 2021-09-09 14:07:45 +02:00
Craig Raw aaca9ffa16 dont allow receiving on badbank wallet, introduce skipInstaller flag in build 2021-09-08 13:22:21 +02:00
Craig Raw 44b9cee825 fix outdated reference to Java 14 and link to Java 16 JDK 2021-09-07 19:27:22 +02:00
Craig Raw c024c351ac minor fixes 2021-09-07 11:51:14 +02:00
Craig Raw ed2914f2aa replace illegal filename characters in wallet name with underscores 2021-09-06 14:49:53 +02:00
Craig Raw 0b4785e01c v1.5.0 beta1 2021-09-06 11:53:34 +02:00
Craig Raw 0c50c9cb9c make jackson jars open and non-synthetic modules 2021-09-06 10:18:36 +02:00
Craig Raw 916c2b6122 fix module issues in binary 2021-09-05 15:47:15 +02:00
Craig Raw 308a89c958 update required java to 16 in build instructions 2021-09-03 20:51:56 +02:00
Craig Raw 88ebef97d4 support mixing from all single sig wallets, handle tor proxy change, and other minor fixes 2021-09-03 17:16:37 +02:00
Craig Raw a42761981c support mixing to multisig wallets 2021-09-02 17:14:01 +02:00
Craig Raw b6f047d382 minor fixes 2021-09-02 15:26:16 +02:00
Craig Raw 8f63d89be8 avoid creating a core wallet if no wallets are open 2021-09-02 15:22:53 +02:00
Craig Raw 6e6111b47c support using stored mix indexes to avoid resending a utxo to the coordinator 2021-09-02 14:12:59 +02:00
Craig Raw e8af7c70bd refactor to WhirlpoolServices 2021-09-02 12:37:07 +02:00
Craig Raw 2fc551e35b add mix to functionality 2021-09-02 11:39:56 +02:00
Craig Raw adb77771aa add mix config persistence and initial usage 2021-09-01 13:10:46 +02:00
Craig Raw 13e01451b7 update nightjar for tor identity change functionality 2021-08-31 16:43:56 +02:00
Craig Raw aa10bcfe1a fixes for encrypted whirlpool wallets and other issues 2021-08-31 16:19:24 +02:00
craigrawandGitHub f30c00ba8f Merge pull request #183 from zeroleak/whirlpool-client-0.23.30-early4
Upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3
2021-08-31 10:57:49 +02:00
zeroleak 4577a64ad5 apply Craig's feedback
- use V2__Whirlpool.sql
- allow pools list & tx0 preview without seed
- filter DataSource wallet for refresh
2021-08-29 10:49:25 +02:00
Craig Raw 7371ca2994 add option to optimize transactions for privacy and display privacy analysis 2021-08-27 16:00:17 +02:00
zeroleak 050c4fc31e upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-27 09:20:41 +02:00
zeroleak 772370808c Merge remote-tracking branch 'remotes/origin/master' into whirlpool-client-0.23.30-early4
# Conflicts:
#	src/main/java/com/sparrowwallet/sparrow/AppServices.java
#	src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java
2021-08-24 12:18:54 +02:00
Craig Raw b22e891b7d make select all, copy on transaction hex area copy untruncated transaction hex 2021-08-24 10:52:56 +02:00
Craig Raw 57b3214c54 improvements to whirlpool dialog 2021-08-24 10:40:27 +02:00
Craig Raw 615b78b497 fix mix event handling for multiple wallets 2021-08-24 09:29:54 +02:00
Craig Raw d1ab1db1c5 fix issues with subtabs in new wallets 2021-08-23 17:27:30 +02:00
Craig Raw f5ac6a3b73 support whirlpool data storage in wallet file, add mixing ui 2021-08-23 16:36:02 +02:00
zeroleak fec45356a2 upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-22 11:26:11 +02:00
Craig Raw 37c4ff4dd7 reduce max number multisig keystores 2021-08-16 17:09:21 +02:00
Craig Raw b0877d94bf make all wallet addresses non-editable once child wallets are added 2021-08-16 13:33:05 +02:00
Craig Raw a3e4342d7d save app width and height across restarts 2021-08-16 11:25:18 +02:00
Craig Raw 90355a653f followup 2021-08-16 11:04:46 +02:00
Craig Raw 6339c3a7d7 allow multisig wallets with many (up to 50) keystores 2021-08-16 10:46:34 +02:00
Craig Raw 8f7f0d4c61 truncate labels over 255 chars before persisting to db 2021-08-16 10:15:14 +02:00
Craig Raw 74b4f51640 add menu items to copy psbt to clipboard in hex or base64 2021-08-16 08:48:14 +02:00
Craig Raw 2caee79df4 initial whirlpool integration 2021-08-12 17:50:13 +02:00
Craig Raw 34b4c39ccd add logging when event wallet id is not present 2021-08-04 10:45:01 +02:00
Craig Raw 34d1571669 fix aopp verification regression on button type 2021-08-04 10:03:18 +02:00
Craig Raw 22465b1b65 followup 2021-08-02 16:10:47 +02:00
Craig Raw b91e8eab51 fix tab loading animation for subtabbed wallets 2021-07-30 15:13:58 +02:00
Craig Raw 55e69bf263 fix naming when using a subtab wallet, import and export wallets with child wallets with db persistence on sparrow exporter 2021-07-30 14:17:21 +02:00
Craig Raw 02d3817cb1 introduce wallet subtabs 2021-07-30 09:46:42 +02:00
Craig Raw fc9cdaabb4 export UTXOs to CSV 2021-07-29 12:09:54 +02:00
Craig Raw 2aa3d83402 fix using the correct address label for the utxo label when recieving a batched tx to several labelled address 2021-07-29 11:40:15 +02:00
Craig Raw fc5d6ada36 add caching for verbose transaction lookups to avoid repeat server requests 2021-07-29 11:09:49 +02:00
Craig Raw be599fb003 fix hummingbird module definition 2021-07-21 15:30:46 +02:00
Craig Raw d79c4a13f6 upgrade to hummingbird v1.6.2 2021-07-20 11:47:56 +02:00
Craig Raw e6fce14fde handle unknown derivation path for unchained caravan key 2021-07-19 11:56:02 +02:00
Craig Raw 5c3a00b71b v1.4.3 2021-07-15 12:02:45 +02:00
Craig Raw ced4d4d337 add caravan import/export, minor ui fixes 2021-07-15 11:09:39 +02:00
Craig Raw 4a3ad9f4ff restrict shown sighash to available values 2021-07-14 15:13:38 +02:00
Craig Raw f5a72105ac minor fixes after refactoring 2021-07-12 08:56:37 +02:00
Craig Raw 0502eec0cd dont show message sign context menu item when message signing cant be performed 2021-07-09 13:35:45 +02:00
Craig Raw 8e6933b5ca increase animation rate and fragment length for UR QRs 2021-07-09 08:23:40 +02:00
Craig Raw 1fd1dec6cf rename tx segwit version field to segwit flag 2021-07-08 11:46:21 +02:00
Craig Raw 422713ff53 fix camera selection issues on linux 2021-07-05 12:58:26 +02:00
Craig Raw ada45ee75b allow selection of webcam from QR scan dialog 2021-07-05 12:33:31 +02:00
Craig Raw 2f153686dd avoid hang on closing webcam due to rescans for new camera devices 2021-07-04 19:18:02 +02:00
Craig Raw f691f1691e add missing requires directive for bwt lib 2021-07-02 13:41:22 +02:00
Craig Raw 1f9e37b40c enable max button for selected utxos without address and label filled 2021-07-02 12:05:38 +02:00
Craig Raw a1d2de1859 use all addressable script types (not just single hash types) 2021-07-02 10:44:24 +02:00
Craig Raw 143472bdfc fix save of address labels on a new wallet 2021-07-01 09:41:56 +02:00
Craig Raw b9e64d42ff accept pasting a whitespace delimited sequence of words into
a textfield of mnemonic word entry
2021-06-30 11:55:58 +02:00
Craig Raw 9a09bb8cda always delete hwi dir on osx before copying 2021-06-30 11:51:31 +02:00
Craig Raw 4b028af123 handle multiple selection when freezing and unfreezing utxos 2021-06-30 09:18:14 +02:00
Craig Raw 8033e5fd88 improve amount error labels adding dust threshold label for too low amounts 2021-06-29 12:48:02 +02:00
Craig Raw badf8c8f2f optimize and increase sampling rate of qr reading 2021-06-29 10:53:22 +02:00
Craig Raw b6a353815c switch mempool size chart to kvB when max Y value is less than 1 MvB 2021-06-28 14:14:22 +02:00
Craig Raw ea2f858dc9 close message sign dialog on platform button and escape key 2021-06-28 13:32:24 +02:00
Craig Raw 324540009a fix detection of input type on finalizing psbt wallet 2021-06-28 13:08:44 +02:00
Craig Raw 1c1f90344f update github build to java 16 2021-06-25 15:18:51 +02:00
Craig Raw 094dd45547 upgrade to gradle 7.1, java 16, javafx 16 2021-06-25 14:56:22 +02:00
Craig Raw 6d434722cc fix prev commit 2021-06-24 12:41:32 +02:00
Craig Raw c8a4ed0c3d add documentation link to bitcoin core error message 2021-06-24 10:07:54 +02:00
Craig Raw c8d997fbf0 v1.4.2 2021-06-23 12:51:38 +02:00
Craig Raw 911ed3a718 improve background text 2021-06-23 12:30:58 +02:00
Craig Raw dbfed31432 add format toggle to message signing dialog (electrum or trezor) 2021-06-22 16:00:15 +02:00
Craig Raw 6f3d4e224e add context menu item to copy transaction hex 2021-06-22 11:47:56 +02:00
Craig Raw 4d6609990c fix error messages on subclassed importers/exporters 2021-06-22 10:13:37 +02:00
Craig Raw 5482196cc7 dark theme improvements 2021-06-22 08:36:16 +02:00
Craig Raw 09f6c9ef81 ensure cleanup of migrated wallets when importing 2021-06-21 11:50:58 +02:00
Craig Raw 9b8f97c041 fix import of sparrow wallet with seed, alphabetically sort import and export choices 2021-06-21 11:25:47 +02:00
Craig Raw c68c713a4b add orig files for keystone and seedsigner 2021-06-18 13:31:08 +02:00
Craig Raw 02e144f802 set keystore label, model and source type when importing a wallet from specter desktop 2021-06-18 11:21:53 +02:00
Craig Raw a9ab4d6c78 Handle importing a wallet from a crypto-account QR using the File > Import Wallet dialog 2021-06-17 14:34:11 +02:00
Craig Raw 5df4e5761c add seedsigner keystore import 2021-06-16 14:46:55 +02:00
186 changed files with 7871 additions and 810 deletions
+4 -9
View File
@@ -12,16 +12,11 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 15.0.2
if: ${{ runner.os == 'Linux' }}
uses: joschi/setup-jdk@v2
- name: Set up JDK 16.0.1
uses: actions/setup-java@v2
with:
java-version: 15
- name: Set up JDK 14.0.2
if: ${{ runner.os == 'Windows' }}
uses: actions/setup-java@v1
with:
java-version: 14.0.2
distribution: 'adopt'
java-version: '16.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:sparrowwallet/drongo.git
url = ../../sparrowwallet/drongo.git
+11 -4
View File
@@ -8,11 +8,18 @@ More information (and release binaries) can be found at https://sparrowwallet.co
## Building
To clone this project, use
To clone this project, use
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
These binaries can be built from source using
`./gradlew jpackage`
@@ -20,7 +27,7 @@ When updating to the latest HEAD
`git pull --recurse-submodules`
All jar files created are reproducible builds.
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
## Running
@@ -28,7 +35,7 @@ If you prefer to run Sparrow directly from source, it can be launched with
`./sparrow`
Java 14 or higher must be installed.
Java 16 or higher must be installed.
## Configuration
+369 -21
View File
@@ -1,11 +1,11 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.kordamp.gradle.jdeps' version '0.9.0'
id 'org.beryx.jlink' version '2.22.0'
id 'extra-java-module-info'
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.4.2'
def sparrowVersion = '1.5.1'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -29,7 +29,7 @@ tasks.withType(AbstractArchiveTask) {
}
javafx {
version = "15"
version = "16"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -38,6 +38,7 @@ 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'
@@ -53,19 +54,28 @@ dependencies {
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0')
implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('com.github.arteam:simple-json-rpc-core:1.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
}
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.0')
implementation('com.sparrowwallet:hummingbird:1.6.2')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
}
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@@ -76,15 +86,19 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.7')
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.18-SNAPSHOT')
testImplementation('junit:junit:4.12')
}
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.MainApp'
}
compileJava {
options.with {
@@ -103,6 +117,10 @@ processResources {
}
}
test {
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
}
run {
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
@@ -120,7 +138,8 @@ run {
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
@@ -135,14 +154,17 @@ jlink {
requires 'java.xml'
requires 'java.logging'
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
@@ -154,17 +176,22 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--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=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
"--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"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
@@ -175,7 +202,7 @@ jlink {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX
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/aopp.properties', '--license-file', 'LICENSE']
if(os.windows) {
@@ -216,3 +243,324 @@ task packageTarDistribution(type: Tar) {
include "Sparrow/**"
}
}
extraJavaModuleInfo {
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
exports('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.core.async')
exports('com.fasterxml.jackson.core.base')
exports('com.fasterxml.jackson.core.exc')
exports('com.fasterxml.jackson.core.filter')
exports('com.fasterxml.jackson.core.format')
exports('com.fasterxml.jackson.core.io')
exports('com.fasterxml.jackson.core.json')
exports('com.fasterxml.jackson.core.json.async')
exports('com.fasterxml.jackson.core.sym')
exports('com.fasterxml.jackson.core.type')
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
requires('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.annotation')
}
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
requires('java.desktop')
requires('java.logging')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('java.sql')
requires('java.xml')
exports('com.fasterxml.jackson.databind')
exports('com.fasterxml.jackson.databind.annotation')
exports('com.fasterxml.jackson.databind.cfg')
exports('com.fasterxml.jackson.databind.deser')
exports('com.fasterxml.jackson.databind.deser.impl')
exports('com.fasterxml.jackson.databind.deser.std')
exports('com.fasterxml.jackson.databind.exc')
exports('com.fasterxml.jackson.databind.ext')
exports('com.fasterxml.jackson.databind.introspect')
exports('com.fasterxml.jackson.databind.json')
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
exports('com.fasterxml.jackson.databind.jsonschema')
exports('com.fasterxml.jackson.databind.jsontype')
exports('com.fasterxml.jackson.databind.jsontype.impl')
exports('com.fasterxml.jackson.databind.module')
exports('com.fasterxml.jackson.databind.node')
exports('com.fasterxml.jackson.databind.ser')
exports('com.fasterxml.jackson.databind.ser.impl')
exports('com.fasterxml.jackson.databind.ser.std')
exports('com.fasterxml.jackson.databind.type')
exports('com.fasterxml.jackson.databind.util')
uses('com.fasterxml.jackson.databind.Module')
}
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
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.3.jar', 'logback.classic', '1.2.3') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
exports('mu')
requires('kotlin.stdlib')
requires('org.slf4j')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
}
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-28.2-jre.jar', 'com.google.common', '28.2-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('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
exports('org.reactfx.util')
requires('javafx.base')
requires('javafx.graphics')
requires('javafx.controls')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
requires('javafx.base')
requires('javafx.graphics')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
}
module('hummingbird-1.6.2.jar', 'com.sparrowwallet.hummingbird', '1.6.2') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.18-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.18-SNAPSHOT') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
requires('org.bouncycastle.provider')
requires('com.fasterxml.jackson.databind')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('org.json')
exports('com.samourai.http.client')
exports('com.samourai.tor.client')
exports('com.samourai.wallet.api.backend')
exports('com.samourai.wallet.api.backend.beans')
exports('com.samourai.wallet.client.indexHandler')
exports('com.samourai.wallet.hd')
exports('com.samourai.wallet.util')
exports('com.samourai.whirlpool.client.event')
exports('com.samourai.whirlpool.client.wallet')
exports('com.samourai.whirlpool.client.wallet.beans')
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
exports('com.samourai.whirlpool.client.whirlpool')
exports('com.samourai.whirlpool.client.whirlpool.beans')
exports('com.samourai.whirlpool.client.wallet.data.pool')
exports('com.samourai.whirlpool.client.wallet.data.utxo')
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
exports('com.samourai.whirlpool.client.wallet.data.supplier')
exports('com.samourai.whirlpool.client.mix.handler')
exports('com.samourai.whirlpool.client.mix.listener')
exports('com.samourai.whirlpool.protocol.beans')
exports('com.samourai.whirlpool.protocol.rest')
exports('com.samourai.whirlpool.client.tx0')
exports('com.samourai.wallet.segwit.bech32')
exports('com.samourai.whirlpool.client.wallet.data.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState')
exports('com.sparrowwallet.nightjar.http')
exports('com.sparrowwallet.nightjar.stomp')
exports('com.sparrowwallet.nightjar.tor')
}
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
exports('com.zeroleak.throwingsupplier')
}
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
exports('com.squareup.okhttp')
}
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
exports('com.auth0.jwt')
}
module('json-20180130.jar', 'org.json', '1.0') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
exports('com.google.protobuf')
}
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
exports('org.apache.commons.text')
}
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
requires('kotlin.stdlib')
requires('commons.compress')
requires('org.tukaani.xz')
requires('java.management')
requires('io.github.microutils.kotlin.logging')
}
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
exports('net.freehaven.tor.control')
}
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
exports('org.apache.commons.compress')
requires('org.tukaani.xz')
}
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
exports('org.tukaani.xz')
}
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
exports('com.runjva.sourceforge.jsocks.protocol')
requires('org.slf4j')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
exports('kotlin.jvm')
exports('kotlin.collections')
}
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')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
}
}
@@ -0,0 +1,54 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}
@@ -0,0 +1,52 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}
@@ -0,0 +1,174 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if (isModule(originalJar) && !originalJarName.contains("jackson")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
if(manifest == null) {
manifest = new Manifest();
}
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
}
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
for (String usesName : moduleInfo.getUses()) {
moduleVisitor.visitUse(usesName.replace('.', '/'));
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
@@ -0,0 +1,62 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
private List<String> uses = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public void uses(String uses) {
this.uses.add(uses);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
protected List<String> getUses() {
return uses;
}
}
+11
View File
@@ -0,0 +1,11 @@
## Sparrow Wallet Repository Docs
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
The documentation here is mainly developer-related resources.
### [Reproducible builds](reproducible.md)
Documentation to create and verify a build of the project against the released binaries.
+78
View File
@@ -0,0 +1,78 @@
## Reproducible builds
Reproducibility is a goal of the Sparrow Wallet project.
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
Work on resolving both of these issues is ongoing.
### Reproducing a release
#### Install Java
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
```shell
sudo apt-get install -y wget apt-transport-https gnupg
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
rm adoptopenjdk-keyring.gpg
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
sudo apt update -y
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
```
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 16.0.1.hs-adpt
```
#### Other requirements
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
```shell
sudo apt install -y rpm fakeroot binutils
```
#### Building the binaries
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.1"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
Thereafter, building should be straightforward:
```shell
cd sparrow
./gradlew jpackage
```
The binaries (and installers) will be placed in the `build/jpackage` folder.
#### Verifying the binaries are identical
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
Then compare all of the folders and files recursively:
```shell
diff -r build/jpackage/Sparrow /tmp/Sparrow
```
This command should have no output indicating that the two folders (and all their contents) are identical.
+1 -1
Submodule drongo updated: 729c3a1435...025af05758
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+4 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
cd `dirname $0`
args="$*"
args="${args%"${args##*[![:space:]]}"}"
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.2</string>
<string>1.5.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.ByteSource;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
@@ -33,16 +30,22 @@ import com.sparrowwallet.sparrow.transaction.TransactionData;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
@@ -117,31 +120,44 @@ public class AppController implements Initializable {
@FXML
private CheckMenuItem openWalletsInNewWindows;
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem hideEmptyUsedAddresses;
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem useHdCameraResolution;
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showLoadingLog;
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showUtxosChart;
private static final BooleanProperty showUtxosChartProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showTxHex;
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
@FXML
private MenuItem minimizeToTray;
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem refreshWallet;
@FXML
private MenuItem sendToMany;
@FXML
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@FXML
private StackPane rootStack;
@@ -160,6 +176,12 @@ public class AppController implements Initializable {
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
private final ChangeListener<Boolean> serverToggleOnlineListener = (observable, oldValue, newValue) -> {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -221,8 +243,8 @@ public class AppController implements Initializable {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.WALLET).map(tabData -> (WalletTabData)tabData).collect(Collectors.toList());
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
.flatMap(tab -> ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData)subTab.getUserData())).collect(Collectors.toList());
if(!closedWalletTabs.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
}
@@ -266,16 +288,26 @@ public class AppController implements Initializable {
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
setTheme(null);
openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows());
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
useHdCameraResolution.setSelected(Config.get().isHdCapture());
showTxHex.setSelected(Config.get().isShowTransactionHex());
showLoadingLog.setSelected(Config.get().isShowLoadingLog());
showUtxosChart.setSelected(Config.get().isShowUtxosChart());
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
showTxHexProperty.set(Config.get().isShowTransactionHex());
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
showUtxosChartProperty.set(Config.get().isShowUtxosChart());
showUtxosChart.selectedProperty().bindBidirectional(showUtxosChartProperty);
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
saveTransaction.setDisable(true);
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
exportWallet.setDisable(true);
lockWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
@@ -283,9 +315,7 @@ public class AppController implements Initializable {
serverToggle.setSelected(isConnected());
serverToggle.setDisable(Config.get().getServerType() == null);
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
});
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
});
@@ -618,13 +648,36 @@ public class AppController implements Initializable {
}
}
public void copyPSBTHex(ActionEvent event) {
copyPSBT(false);
}
public void copyPSBTBase64(ActionEvent event) {
copyPSBT(true);
}
public void copyPSBT(boolean asBase64) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
ClipboardContent content = new ClipboardContent();
content.putString(data);
Clipboard.getSystemClipboard().setContent(content);
}
}
public List<WalletTabData> getOpenWalletTabData() {
List<WalletTabData> openWalletTabData = new ArrayList<>();
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
openWalletTabData.add((WalletTabData)tabData);
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
openWalletTabData.add((WalletTabData)subTab.getUserData());
}
}
}
@@ -643,11 +696,14 @@ public class AppController implements Initializable {
public void selectTab(Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(walletTabData.getWallet() == wallet) {
tabs.getSelectionModel().select(tab);
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
if(walletTabData.getWallet() == wallet) {
tabs.getSelectionModel().select(tab);
subTabs.getSelectionModel().select(subTab);
}
}
}
}
@@ -707,6 +763,12 @@ public class AppController implements Initializable {
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
}
public void preventSleep(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setPreventSleep(item.isSelected());
AppServices.get().setPreventSleep(item.isSelected());
}
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
@@ -801,7 +863,10 @@ public class AppController implements Initializable {
} catch(StorageException e) {
showErrorDialog("Error Opening Wallet", e.getMessage());
} catch(Exception e) {
if(!attemptImportWallet(file, null)) {
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
} else if(!attemptImportWallet(file, null)) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
}
@@ -811,13 +876,13 @@ public class AppController implements Initializable {
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
try {
checkWalletNetwork(walletBackupAndKey.getWallet());
restorePublicKeysFromSeed(walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
restorePublicKeysFromSeed(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
if(!walletBackupAndKey.getWallet().isValid()) {
throw new IllegalStateException("Wallet file is not valid.");
}
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
for(Map.Entry<Storage, WalletBackupAndKey> entry : walletBackupAndKey.getChildWallets().entrySet()) {
openWallet(entry.getKey(), entry.getValue(), walletAppController, true);
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
}
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
} catch(Exception e) {
@@ -834,19 +899,24 @@ public class AppController implements Initializable {
}
}
private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
if(wallet.containsPrivateKeys()) {
//Derive xpub and master fingerprint from seed, potentially with passphrase
Wallet copy = wallet.copy();
for(Keystore copyKeystore : copy.getKeystores()) {
for(int i = 0; i < copy.getKeystores().size(); i++) {
Keystore copyKeystore = copy.getKeystores().get(i);
if(copyKeystore.hasSeed()) {
if(copyKeystore.getSeed().needsPassphrase()) {
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getName(), copyKeystore);
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
if(optionalPassphrase.isPresent()) {
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
} else {
return;
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullName(), copyKeystore);
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
if(optionalPassphrase.isPresent()) {
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
} else {
return;
}
}
} else {
copyKeystore.getSeed().setPassphrase("");
@@ -862,6 +932,18 @@ public class AppController implements Initializable {
copy.decrypt(key);
}
if(wallet.isWhirlpoolMasterWallet()) {
String walletId = storage.getWalletId(wallet);
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
}
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
}
for(int i = 0; i < wallet.getKeystores().size(); i++) {
Keystore keystore = wallet.getKeystores().get(i);
if(keystore.hasSeed()) {
@@ -897,7 +979,8 @@ public class AppController implements Initializable {
new SpecterDesktop(),
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig(),
new KeystoneSinglesig(), new KeystoneMultisig());
new KeystoneSinglesig(), new KeystoneMultisig(),
new CaravanMultisig());
for(WalletImport importer : walletImporters) {
try(FileInputStream inputStream = new FileInputStream(file)) {
if(importer.isEncrypted(file) && password == null) {
@@ -949,11 +1032,13 @@ public class AppController implements Initializable {
//Close existing wallet first if open
for(Iterator<Tab> iter = tabs.getTabs().iterator(); iter.hasNext(); ) {
Tab tab = iter.next();
TabData tabData = (TabData)tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(walletTabData.getStorage().getWalletFile().equals(walletFile)) {
iter.remove();
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
if(walletTabData.getStorage().getWalletFile().equals(walletFile)) {
iter.remove();
}
}
}
}
@@ -961,6 +1046,10 @@ public class AppController implements Initializable {
walletFile.delete();
}
if(wallet.isEncrypted()) {
throw new IllegalArgumentException("Imported wallet must be unencrypted");
}
Storage storage = new Storage(Storage.getWalletFile(wallet.getName()));
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.UPDATE_NEW);
Optional<SecureString> password = dlg.showAndWait();
@@ -969,8 +1058,18 @@ public class AppController implements Initializable {
try {
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, null);
addWalletTabOrWindow(storage, wallet, null, false);
} catch(IOException | StorageException e) {
for(Wallet childWallet : wallet.getChildWallets()) {
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, null);
addWalletTabOrWindow(storage, childWallet, null, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
log.error("Error saving imported wallet", e);
}
} else {
@@ -986,8 +1085,19 @@ public class AppController implements Initializable {
wallet.encrypt(key);
storage.setEncryptionPubKey(encryptionPubKey);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, key);
addWalletTabOrWindow(storage, wallet, null, false);
} catch(IOException | StorageException e) {
for(Wallet childWallet : wallet.getChildWallets()) {
childWallet.encrypt(key);
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key);
addWalletTabOrWindow(storage, childWallet, null, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
log.error("Error saving imported wallet", e);
} finally {
encryptionFullKey.clear();
@@ -1007,11 +1117,9 @@ public class AppController implements Initializable {
}
public void exportWallet(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData)tabData;
WalletExportDialog dlg = new WalletExportDialog(walletTabData.getWallet());
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm.getWallet());
Optional<Wallet> wallet = dlg.showAndWait();
if(wallet.isPresent()) {
//Successful export
@@ -1026,10 +1134,9 @@ public class AppController implements Initializable {
public void signVerifyMessage(ActionEvent event) {
MessageSignDialog messageSignDialog = null;
Tab tab = tabs.getSelectionModel().getSelectedItem();
if(tab != null && tab.getUserData() instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tab.getUserData();
Wallet wallet = walletTabData.getWallet();
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
if(wallet.getKeystores().size() == 1 &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
//Can sign and verify
@@ -1046,11 +1153,9 @@ public class AppController implements Initializable {
}
public void sendToMany(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
Wallet wallet = walletTabData.getWallet();
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
BitcoinUnit bitcoinUnit = Config.get().getBitcoinUnit();
if(bitcoinUnit == BitcoinUnit.AUTO) {
bitcoinUnit = wallet.getAutoUnit();
@@ -1071,17 +1176,22 @@ public class AppController implements Initializable {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
public void lockWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
EventManager.get().post(new WalletLockEvent(selectedWalletForm.getMasterWallet()));
}
}
public void refreshWallet(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
Wallet wallet = walletTabData.getWallet();
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
Wallet pastWallet = wallet.copy();
walletTabData.getStorage().backupTempWallet();
selectedWalletForm.getStorage().backupTempWallet();
wallet.clearHistory();
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, walletTabData.getWalletForm().getWalletId()));
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, selectedWalletForm.getWalletId()));
}
}
@@ -1109,7 +1219,7 @@ public class AppController implements Initializable {
}
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
try {
if(wallet.isMasterWallet()) {
String name = storage.getWalletName(wallet);
if(!name.equals(wallet.getName())) {
wallet.setName(name);
@@ -1122,10 +1232,91 @@ public class AppController implements Initializable {
tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
tab.setOnCloseRequest(event -> {
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent() && optType.get() == ButtonType.NO) {
event.consume();
}
}
});
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
setSubTabsVisible(subTabs, false);
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
if(selectedTab != null) {
EventManager.get().post(new WalletTabSelectedEvent(tab));
}
});
subTabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
});
tabs.getTabs().add(tab);
tabs.getSelectionModel().select(tab);
} else {
for(Tab walletTab : tabs.getTabs()) {
TabData tabData = (TabData)walletTab.getUserData();
if(tabData instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tabData;
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet, backupWallet);
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
Platform.runLater(() -> {
setSubTabsVisible(subTabs, true);
});
}
}
}
}
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
}
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
if(visible) {
subTabs.getStyleClass().remove("master-only");
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
subTabs.getStyleClass().add("wallet-subtabs");
}
} else {
if(!subTabs.getStyleClass().contains("master-only")) {
subTabs.getStyleClass().add("master-only");
}
subTabs.getStyleClass().remove("wallet-subtabs");
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
try {
Tab subTab = new Tab();
subTab.setClosable(false);
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
subTabLabel.setGraphic(getSubTabGlyph(wallet));
subTabLabel.setContentDisplay(ContentDisplay.TOP);
subTabLabel.setAlignment(Pos.TOP_CENTER);
subTab.setGraphic(subTabLabel);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
tab.setContent(walletLoader.load());
subTab.setContent(walletLoader.load());
WalletController controller = walletLoader.getController();
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
@@ -1136,15 +1327,57 @@ public class AppController implements Initializable {
controller.setWalletForm(walletForm);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
subTab.setUserData(tabData);
tabs.getTabs().add(tab);
tabs.getSelectionModel().select(tab);
subTabs.getTabs().add(subTab);
subTabs.getTabs().sort((o1, o2) -> {
WalletTabData tabData1 = (WalletTabData) o1.getUserData();
WalletTabData tabData2 = (WalletTabData) o2.getUserData();
return tabData1.getWallet().compareTo(tabData2.getWallet());
});
subTabs.getSelectionModel().select(subTab);
return walletForm;
} catch(IOException e) {
throw new RuntimeException(e);
}
}
private Glyph getSubTabGlyph(Wallet wallet) {
Glyph tabGlyph;
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount == StandardAccount.WHIRLPOOL_PREMIX) {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
} else if(standardAccount == StandardAccount.WHIRLPOOL_POSTMIX) {
tabGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
} else if(standardAccount == StandardAccount.WHIRLPOOL_BADBANK) {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
} else {
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
}
tabGlyph.setFontSize(12);
return tabGlyph;
}
private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex();
return (account < 0 || account > 9) ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
}
public WalletForm getSelectedWalletForm() {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane)selectedTab.getContent();
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
return subWalletTabData.getWalletForm();
}
return null;
}
public void openExamples(ActionEvent event) {
try {
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
@@ -1311,6 +1544,18 @@ public class AppController implements Initializable {
private ContextMenu getTabContextMenu(Tab tab) {
ContextMenu contextMenu = new ContextMenu();
if(tab.getUserData() instanceof WalletTabData walletTabData) {
MenuItem lock = new MenuItem("Lock");
Glyph lockGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lockGlyph.setFontSize(12);
lock.setGraphic(lockGlyph);
lock.disableProperty().bind(walletTabData.getWalletForm().lockedProperty());
lock.setOnAction(event -> {
EventManager.get().post(new WalletLockEvent(walletTabData.getWallet()));
});
contextMenu.getItems().addAll(lock);
}
MenuItem close = new MenuItem("Close");
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
@@ -1380,7 +1625,7 @@ public class AppController implements Initializable {
}
private void tabLabelStartAnimation(Wallet wallet) {
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStartAnimation);
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWallet()).anyMatch(tabWallet -> tabWallet == wallet)).forEach(this::tabLabelStartAnimation);
}
private void tabLabelStartAnimation(Transaction transaction) {
@@ -1408,7 +1653,15 @@ public class AppController implements Initializable {
}
private void tabLabelStopAnimation(Wallet wallet) {
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStopAnimation);
Set<Wallet> relatedWallets = new HashSet<>(wallet.isMasterWallet() ? wallet.getChildWallets() : wallet.getMasterWallet().getChildWallets());
relatedWallets.remove(wallet);
if(!wallet.isMasterWallet()) {
relatedWallets.add(wallet.getMasterWallet());
}
if(loadingWallets.stream().noneMatch(relatedWallets::contains)) {
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWallet()).anyMatch(tabWallet -> tabWallet == wallet)).forEach(this::tabLabelStopAnimation);
}
}
private void tabLabelStopAnimation(Transaction transaction) {
@@ -1430,6 +1683,29 @@ public class AppController implements Initializable {
tabLabel.getGraphic().getStyleClass().remove("failure");
}
private void setTorIcon() {
TorStatusLabel torStatusLabel = null;
for(Node node : statusBar.getRightItems()) {
if(node instanceof TorStatusLabel) {
torStatusLabel = (TorStatusLabel)node;
}
}
if(!AppServices.isUsingProxy()) {
if(torStatusLabel != null) {
torStatusLabel.update();
statusBar.getRightItems().removeAll(torStatusLabel);
}
} else {
if(torStatusLabel == null) {
torStatusLabel = new TorStatusLabel();
statusBar.getRightItems().add(Math.max(statusBar.getRightItems().size() - 2, 0), torStatusLabel);
} else {
torStatusLabel.update();
}
}
}
@Subscribe
public void themeChanged(ThemeChangedEvent event) {
String darkCss = getClass().getResource("darktheme.css").toExternalForm();
@@ -1465,6 +1741,7 @@ public class AppController implements Initializable {
} else {
saveTransaction.setVisible(false);
}
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
@@ -1474,6 +1751,7 @@ public class AppController implements Initializable {
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
@@ -1498,11 +1776,9 @@ public class AppController implements Initializable {
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
Tab tab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tabData;
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId())) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
exportWallet.setDisable(!event.getWallet().isValid());
}
}
@@ -1511,9 +1787,44 @@ public class AppController implements Initializable {
@Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) {
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
String text;
if(event.getBlockTransactions().size() == 1) {
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
TabData tabData = (TabData)subTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWallet().equals(event.getWallet()) && walletTabData.getWalletForm().lockedProperty().get()) {
return;
}
}
}
}
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
if(!whirlpoolTransactions.isEmpty()) {
BlockTransaction blockTransaction = whirlpoolTransactions.get(0);
String status;
String walletName = event.getWallet().getMasterName() + " " + event.getWallet().getName().toLowerCase();
long value = blockTransaction.getTransaction().getOutputs().iterator().next().getValue();
long mempoolValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() <= 0).mapToLong(tx -> value).sum();
long blockchainValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() > 0).mapToLong(tx -> value).sum();
if(mempoolValue > 0) {
status = "New " + walletName + " mempool transaction" + (mempoolValue > value ? "s: " : ": ") + event.getValueAsText(mempoolValue);
} else {
status = "Confirming " + walletName + " transaction" + (blockchainValue > value ? "s: " : ": ") + event.getValueAsText(blockchainValue);
}
statusUpdated(new StatusEvent(status));
}
String text = null;
if(blockTransactions.size() == 1) {
BlockTransaction blockTransaction = blockTransactions.get(0);
if(blockTransaction.getHeight() <= 0) {
text = "New mempool transaction: ";
} else {
@@ -1528,9 +1839,9 @@ public class AppController implements Initializable {
}
text += event.getValueAsText(event.getTotalValue());
} else {
} else if(blockTransactions.size() > 1) {
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total";
} else if(event.getTotalMempoolValue() > 0) {
text = "New mempool transactions: " + event.getValueAsText(event.getTotalMempoolValue()) + " total";
} else {
@@ -1538,29 +1849,39 @@ public class AppController implements Initializable {
}
}
Window.getWindows().forEach(window -> {
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
window.getScene().getStylesheets().add(notificationStyles);
if(text != null) {
Window.getWindows().forEach(window -> {
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
window.getScene().getStylesheets().add(notificationStyles);
}
});
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
String walletName = event.getWallet().getMasterName();
if(walletName.length() > 25) {
walletName = walletName.substring(0, 25) + "...";
}
if(!event.getWallet().isMasterWallet()) {
walletName += " " + event.getWallet().getName();
}
});
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + event.getWallet().getName())
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.onAction(e -> selectTab(event.getWallet()));
Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + walletName)
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.onAction(e -> selectTab(event.getWallet()));
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
}
notificationBuilder.show();
}
notificationBuilder.show();
}
}
@@ -1658,6 +1979,7 @@ public class AppController implements Initializable {
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
statusUpdated(new StatusEvent(status));
serverToggleStopAnimation();
setTorIcon();
}
@Subscribe
@@ -1666,6 +1988,7 @@ public class AppController implements Initializable {
statusUpdated(new StatusEvent(status));
setServerToggleTooltip(event.getBlockHeight());
serverToggleStopAnimation();
setTorIcon();
}
@Subscribe
@@ -1685,7 +2008,8 @@ public class AppController implements Initializable {
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(loadingWallets::remove) && loadingWallets.isEmpty()) {
event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).forEach(loadingWallets::remove);
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(emptyLoadingWallets::remove) && emptyLoadingWallets.isEmpty()) {
if(statusBar.getText().equals(LOADING_TRANSACTIONS_MESSAGE)) {
statusBar.setText("");
}
@@ -1711,9 +2035,10 @@ public class AppController implements Initializable {
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(-1);
loadingWallets.add(event.getWallet());
emptyLoadingWallets.add(event.getWallet());
}
}
loadingWallets.add(event.getWallet());
tabLabelStartAnimation(event.getWallet());
}
}
@@ -1727,8 +2052,9 @@ public class AppController implements Initializable {
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0);
}
tabLabelStopAnimation(event.getWallet());
emptyLoadingWallets.remove(event.getWallet());
loadingWallets.remove(event.getWallet());
tabLabelStopAnimation(event.getWallet());
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == event.getWallet()).forEach(this::tabLabelRemoveFailure);
}
}
@@ -1789,18 +2115,21 @@ public class AppController implements Initializable {
public void torBootStatus(TorBootStatusEvent event) {
serverToggle.setDisable(true);
statusUpdated(new StatusEvent(event.getStatus(), 120));
setTorIcon();
}
@Subscribe
public void torFailedStatus(TorFailedStatusEvent event) {
serverToggle.setDisable(false);
statusUpdated(new StatusEvent(event.getStatus()));
setTorIcon();
}
@Subscribe
public void torReadyStatus(TorReadyStatusEvent event) {
serverToggle.setDisable(false);
statusUpdated(new StatusEvent(event.getStatus()));
setTorIcon();
}
@Subscribe
@@ -1818,23 +2147,29 @@ public class AppController implements Initializable {
public void viewWallet(ViewWalletEvent event) {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(event.getStorage().getWalletId(event.getWallet()).equals(walletTabData.getWalletForm().getWalletId())) {
tabs.getSelectionModel().select(tab);
return;
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
if(event.getStorage().getWalletId(event.getWallet()).equals(walletTabData.getWalletForm().getWalletId())) {
tabs.getSelectionModel().select(tab);
subTabs.getSelectionModel().select(subTab);
return;
}
}
}
}
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(event.getStorage().getWalletFile().equals(walletTabData.getStorage().getWalletFile())) {
tabs.getSelectionModel().select(tab);
return;
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
if(event.getStorage().getWalletFile().equals(walletTabData.getStorage().getWalletFile())) {
tabs.getSelectionModel().select(tab);
subTabs.getSelectionModel().select(subTab);
return;
}
}
}
}
@@ -1914,4 +2249,30 @@ public class AppController implements Initializable {
public void recieveAction(ReceiveActionEvent event) {
selectTab(event.getWallet());
}
@Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) {
Storage storage = AppServices.get().getOpenWallets().get(event.getWallet());
if(storage == null) {
throw new IllegalStateException("Cannot find storage for master wallet");
}
addWalletTab(storage, event.getChildWallet(), null);
}
@Subscribe
public void walletLock(WalletLockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
}
}
}
@@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
@@ -15,12 +16,14 @@ import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -45,6 +48,7 @@ import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenURIHandler;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.*;
@@ -71,6 +75,8 @@ public class AppServices {
private static AppServices INSTANCE;
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
private final MainApp application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
@@ -89,8 +95,12 @@ public class AppServices {
private TorService torService;
private ScheduledService<Void> preventSleepService;
private static Integer currentBlockHeight;
private static BlockHeader latestBlockHeader;
private static Map<Integer, Double> targetBlockFeeRates;
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
@@ -112,6 +122,11 @@ public class AppServices {
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
if(online) {
if(Config.get().requiresInternalTor() && !isTorRunning()) {
if(torService.getState() == Worker.State.SCHEDULED) {
torService.cancel();
torService.reset();
}
torService.start();
} else {
restartServices();
@@ -135,6 +150,7 @@ public class AppServices {
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
}
public void start() {
@@ -143,6 +159,7 @@ public class AppServices {
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
@@ -170,6 +187,10 @@ public class AppServices {
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
restartService(versionCheckService);
}
if(config.isPreventSleep()) {
restartService(preventSleepService);
}
}
private void restartService(ScheduledService<?> service) {
@@ -369,6 +390,39 @@ public class AppServices {
return torService;
}
private ScheduledService<Void> createPreventSleepService() {
ScheduledService<Void> preventSleepService = new ScheduledService<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() {
try {
Robot robot = new Robot();
robot.keyRelease(KeyEvent.VK_F16);
} catch(Exception e) {
log.debug("Error preventing sleep", e);
}
return null;
}
};
}
};
preventSleepService.setPeriod(Duration.minutes(1));
return preventSleepService;
}
public void setPreventSleep(boolean preventSleep) {
if(preventSleepService != null) {
if(preventSleep) {
restartService(preventSleepService);
} else {
preventSleepService.cancel();
}
}
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
@@ -413,6 +467,10 @@ public class AppServices {
return INSTANCE;
}
public static WhirlpoolServices getWhirlpoolServices() {
return get().whirlpoolServices;
}
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
@@ -473,6 +531,10 @@ public class AppServices {
return openWallets;
}
public Wallet getWallet(String walletId) {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public Window getWindowForWallet(String walletId) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
@@ -503,6 +565,10 @@ public class AppServices {
return currentBlockHeight;
}
public static BlockHeader getLatestBlockHeader() {
return latestBlockHeader;
}
public static Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
@@ -585,7 +651,7 @@ public class AppServices {
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3) {
alert.getDialogPane().setPrefHeight(180 + lines.length * 20);
alert.getDialogPane().setPrefHeight(200 + lines.length * 20);
}
moveToActiveWindowScreen(alert);
@@ -773,9 +839,11 @@ public class AppServices {
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader();
}
@Subscribe
@@ -786,6 +854,8 @@ public class AppServices {
@Subscribe
public void newBlock(NewBlockEvent event) {
currentBlockHeight = event.getHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
latestBlockHeader = event.getBlockHeader();
String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status));
}
@@ -848,7 +918,7 @@ public class AppServices {
Platform.runLater(() -> {
if(!Window.getWindows().isEmpty()) {
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).filter(File::exists).collect(Collectors.toList());
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
}
});
@@ -921,7 +991,7 @@ public class AppServices {
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
onlineProperty.set(false);
log.info("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
@@ -31,8 +31,8 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.4.2";
public static final String APP_VERSION_SUFFIX = "-beta";
public static final String APP_VERSION = "1.5.1";
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";
@@ -42,7 +42,13 @@ public class MainApp extends Application {
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
} else {
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
}
});
super.init();
}
@@ -64,7 +70,6 @@ public class MainApp extends Application {
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
Config.get().setCoreWallet(Bwt.DEFAULT_CORE_WALLET);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
@@ -79,9 +84,6 @@ public class MainApp extends Application {
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE && Config.get().getCoreWallet() == null) {
Config.get().setCoreMultiWallet(Boolean.TRUE);
Config.get().setCoreWallet("");
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
@@ -90,6 +92,11 @@ public class MainApp extends Application {
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
mainStage.setWidth(Config.get().getAppWidth());
mainStage.setHeight(Config.get().getAppHeight());
}
AppController appController = AppServices.newAppWindow(stage);
if(createNewWallet) {
@@ -119,6 +126,8 @@ public class MainApp extends Application {
@Override
public void stop() throws Exception {
AppServices.get().stop();
Config.get().setAppWidth(mainStage.getWidth());
Config.get().setAppHeight(mainStage.getHeight());
mainStage.close();
if(sparrowInstance != null) {
sparrowInstance.freeLock();
@@ -2,11 +2,16 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,91 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
public class AddAccountDialog extends Dialog<StandardAccount> {
private final ComboBox<StandardAccount> standardAccountCombo;
public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
setTitle("Add Account");
dialogPane.setHeaderText("Choose an account to add:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
key.setFontSize(50);
dialogPane.setGraphic(key);
final VBox content = new VBox(10);
content.setPrefHeight(50);
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
List<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
}
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
}
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() {
@Override
public String toString(StandardAccount account) {
if(account == null) {
return "None Available";
}
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
return "Whirlpool Accounts";
}
return account.getName();
}
@Override
public StandardAccount fromString(String string) {
return null;
}
});
if(standardAccountCombo.getItems().isEmpty()) {
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
okButton.setDisable(true);
} else {
standardAccountCombo.getSelectionModel().select(0);
}
content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null);
}
}
@@ -11,55 +11,44 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, Entry> {
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("address-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
super.updateItem(addressStatus, empty);
EntryCell.applyRowStyles(this, entry);
getStyleClass().add("address-cell");
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
EntryCell.applyRowStyles(this, utxoEntry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
if(utxoEntry.isDuplicateAddress()) {
if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
setGraphic(getDuplicateGlyph());
Tooltip tt = new Tooltip();
tt.setText(getTooltipText(utxoEntry));
setTooltip(tt);
} else {
setGraphic(null);
}
});
}
}
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -34,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
addressCol.setSortable(false);
getColumns().add(addressCol);
if(address != null) {
if(address != null && !rootEntry.getWallet().isWhirlpoolChildWallet()) {
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
}
@@ -68,21 +68,23 @@ public class AddressTreeTable extends CoinTreeTable {
}
}
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
}
}
}
}
}
});
});
}
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
@@ -34,11 +34,15 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
} else {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
setContextMenu(null);
} else if(utxoEntry.getHashIndex().getDate() != null) {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
} else {
setText("Unknown");
setContextMenu(null);
}
Tooltip tooltip = new Tooltip();
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
}
public static String getShortDateFormat(Date date) {
if(date == null) {
return "Unknown";
}
Date now = new Date();
long elapsed = (now.getTime() - date.getTime()) / 1000;
@@ -52,7 +52,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private SplitMenuButton importButton;
private ButtonBase importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
@@ -61,13 +61,13 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.keyDerivation = requiredDerivation;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -199,35 +199,39 @@ public class DevicePane extends TitledDescriptionPane {
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
});
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
}
}
importButton.managedProperty().bind(importButton.visibleProperty());
@@ -430,7 +434,9 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(true);
} else {
showOperationButton();
setContent(getTogglePassphraseOn());
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
setContent(getTogglePassphraseOn());
}
}
} else {
setError("Incorrect PIN", null);
@@ -546,7 +552,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.SINGLE_HASH_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
@@ -622,7 +628,8 @@ public class DevicePane extends TitledDescriptionPane {
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
@@ -642,6 +649,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@@ -651,7 +659,8 @@ public class DevicePane extends TitledDescriptionPane {
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import");
Button importDerivationButton = new Button("Import Custom Derivation");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -660,7 +669,8 @@ public class DevicePane extends TitledDescriptionPane {
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
@@ -118,8 +119,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
actionBox.getChildren().add(receiveButton);
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
(nodeEntry.getWallet().getKeystores().get(0).hasPrivateKey() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
if(canSignMessage(nodeEntry.getWallet())) {
Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> {
@@ -127,10 +127,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
messageSignDialog.showAndWait();
});
actionBox.getChildren().add(signMessageButton);
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
}
setGraphic(actionBox);
if(nodeEntry.getWallet().isWhirlpoolChildWallet()) {
setText(address.toString().substring(0, 20) + "...");
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
@@ -268,6 +273,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(Wallet wallet) {
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
return transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
@@ -292,14 +302,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
}
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() != Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(null);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() == Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(null));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static Glyph getViewTransactionGlyph() {
@@ -449,7 +473,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().add(receiveToAddress);
if(nodeEntry != null) {
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
@@ -514,7 +538,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
freezeUtxo.setGraphic(getFreezeGlyph());
freezeUtxo.setOnAction(AE -> {
hide();
freezeUtxo(hashIndexEntry);
freezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(freezeUtxo);
} else {
@@ -522,7 +546,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
unfreezeUtxo.setOnAction(AE -> {
hide();
unfreezeUtxo(hashIndexEntry);
unfreezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(unfreezeUtxo);
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
@@ -12,11 +13,13 @@ import java.io.*;
public class FileKeystoreImportPane extends FileImportPane {
protected final Wallet wallet;
private final KeystoreFileImport importer;
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
@@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
EventManager.get().post(new KeystoreImportEvent(keystore));
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
}
@@ -80,9 +80,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(wallet);
fileChooser.setInitialFileName(wallet.getName() +
(exporter instanceof Sparrow ? "" : "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "")) +
(extension == null || extension.isEmpty() ? "" : "." + extension));
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
if(exporter instanceof Sparrow) {
fileName = wallet.getMasterName();
}
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
@@ -94,7 +96,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file) {
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
Wallet copy = wallet.copy();
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
@@ -27,6 +28,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
@@ -42,35 +45,62 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
}
} else {
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
}
}
setContent(getScriptTypeEntry());
setContent(getScriptTypeEntry(scriptTypes));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Wallet wallet = new Wallet();
wallet.setName(fileName);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry() {
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
@@ -2,13 +2,13 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
@@ -19,6 +19,7 @@ public class HelpLabel extends Label {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
getStyleClass().add("help-label");
@@ -26,10 +27,10 @@ public class HelpLabel extends Label {
}
private static Glyph getHelpGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
lockGlyph.getStyleClass().add("help-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
glyph.getStyleClass().add("help-icon");
glyph.setFontSize(11);
return glyph;
}
public final StringProperty helpTextProperty() {
@@ -49,4 +50,18 @@ public class HelpLabel extends Label {
public final String getHelpText() {
return helpText == null ? "" : helpText.getValue();
}
public ObjectProperty<Node> helpGraphicProperty() {
if(helpGraphicProperty == null) {
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
}
return helpGraphicProperty;
}
private ObjectProperty<Node> helpGraphicProperty;
public final void setHelpGraphic(Node graphic) {
helpGraphicProperty().setValue(graphic);
}
}
@@ -7,6 +7,7 @@ import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
@@ -20,11 +21,15 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
this(walletName, keystore, false);
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
@@ -40,6 +45,13 @@ public class KeystorePassphraseDialog extends Dialog<String> {
content.setPrefHeight(50);
content.getChildren().add(passphrase);
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warnGlyph.getStyleClass().add("warn-icon");
warnGlyph.setFontSize(12);
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
warnLabel.setGraphicTextGap(5);
content.getChildren().add(warnLabel);
dialogPane.setContent(content);
Platform.runLater(passphrase::requestFocus);
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
public static final int MAX_PERIOD_HOURS = 2;
private static final double Y_VALUE_BREAK_MVB = 3.0;
private Tooltip tooltip;
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
categoryAxis.setTickLabelRotation(0);
NumberAxis numberAxis = (NumberAxis)getYAxis();
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
return (vSizeBytes / (1000 * 1000)) + " MvB";
}
@Override
public Number fromString(String string) {
return null;
}
});
this.setOnMouseMoved(mouseEvent -> {
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
previousFeeRate = feeRate;
}
final double maxMvB = getMaxMvB(getData());
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
if(maxMvB > Y_VALUE_BREAK_MVB) {
return (vSizeBytes / (1000 * 1000)) + " MvB";
} else {
return (vSizeBytes / (1000)) + " kvB";
}
}
@Override
public Number fromString(String string) {
return null;
}
});
if(categories.keySet().iterator().hasNext()) {
String time = categories.values().iterator().next();
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
return categories;
}
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
double maxTotal = 0d;
for(Series<String, Number> series : seriesList) {
maxTotal = Math.max(maxTotal, getMaxMvB(series));
}
return maxTotal;
}
private static double getMaxMvB(Series<String, Number> series) {
double total = 0d;
for(XYChart.Data<String, Number> data : series.getData()) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
total += mvb;
}
return total;
}
private static class ChartTooltip extends VBox {
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
Label title = new Label("At " + time);
HBox titleBox = new HBox(title);
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
getChildren().add(titleBox);
double maxMvB = getMaxMvB(seriesList);
for(int i = seriesList.size() - 1; i >= 0; i--) {
Series<String, Number> series = seriesList.get(i);
for(XYChart.Data<String, Number> data : series.getData()) {
if(data.getXValue().equals(category)) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
if(mvb >= 0.01) {
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
double kvb = data.getYValue().doubleValue() / 1000;
double mvb = kvb / 1000;
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
Label label = new Label(series.getName() + ": " + amount);
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
@@ -24,6 +24,7 @@ import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
@@ -44,9 +45,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final ToggleGroup formatGroup;
private final ToggleButton formatTrezor;
private final ToggleButton formatElectrum;
private final Wallet wallet;
private WalletNode walletNode;
private boolean electrumSignatureFormat;
private boolean closed;
/**
* Verification only constructor
@@ -144,7 +149,22 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
signature.setWrapText(true);
signatureField.getInputs().add(signature);
fieldset.getChildren().addAll(addressField, messageField, signatureField);
Field formatField = new Field();
formatField.setText("Format:");
formatGroup = new ToggleGroup();
formatElectrum = new ToggleButton("Standard (Electrum)");
formatTrezor = new ToggleButton("BIP137 (Trezor)");
SegmentedButton formatButtons = new SegmentedButton(formatElectrum, formatTrezor);
formatButtons.setToggleGroup(formatGroup);
formatField.getInputs().add(formatButtons);
formatGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
electrumSignatureFormat = (newValue == formatElectrum);
});
formatButtons.setDisable(wallet != null && walletNode != null && wallet.getScriptType() == ScriptType.P2PKH);
fieldset.getChildren().addAll(addressField, messageField, signatureField, formatField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
@@ -153,11 +173,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
address.setEditable(false);
message.setEditable(false);
signature.setEditable(false);
formatButtons.setDisable(true);
}
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
if(buttons.length > 0) {
dialogPane.getButtonTypes().addAll(buttons);
@@ -220,11 +241,25 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
EventManager.get().unregister(this);
if(!closed) {
EventManager.get().unregister(this);
closed = true;
}
});
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
Platform.runLater(() -> {
if(address.getText().isEmpty()) {
address.requestFocus();
} else if(message.getText().isEmpty()) {
message.requestFocus();
}
formatGroup.selectToggle(formatElectrum);
});
}
private Address getAddress()throws InvalidAddressException {
@@ -241,13 +276,14 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
* @param electrumSignatureFormat
*/
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
formatGroup.selectToggle(electrumSignatureFormat ? formatElectrum : formatTrezor);
this.electrumSignatureFormat = electrumSignatureFormat;
}
private boolean isValidAddress() {
try {
getAddress();
return true;
Address address = getAddress();
return address.getScriptType() != ScriptType.P2TR;
} catch (InvalidAddressException e) {
return false;
}
@@ -280,7 +316,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
String signatureText = privKey.signMessage(message.getText().trim(), scriptType, null);
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
signature.clear();
signature.appendText(signatureText);
privKey.clear();
@@ -363,7 +399,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
@@ -0,0 +1,179 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
setContentDisplay(ContentDisplay.LEFT);
setGraphicTextGap(8);
getStyleClass().add("mixstatus-cell");
}
@Override
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
super.updateItem(mixStatus, empty);
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
if(empty || mixStatus == null) {
setText(null);
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
private void setMixSuccess(Utxo nextMixUtxo) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason, String mixError) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
} else {
setGraphic(null);
setTooltip(null);
}
}
private String getPlatformSleepConfig() {
Platform platform = Platform.getCurrent();
if(platform == Platform.OSX) {
return "OSX System Preferences";
} else if(platform == Platform.WINDOWS) {
return "Windows Control Panel";
}
return "system power settings";
}
private void setMixProgress(MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
setTooltip(tt);
} else {
setGraphic(null);
setTooltip(null);
}
}
private ProgressIndicator getProgressIndicator() {
ProgressIndicator progressIndicator;
if(getGraphic() instanceof ProgressIndicator) {
progressIndicator = (ProgressIndicator)getGraphic();
} else {
progressIndicator = new ProgressBar();
}
return progressIndicator;
}
private static Glyph getMixGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getStopGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
public static Glyph getFailGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.getStyleClass().add("fail-warning");
failGlyph.setFontSize(12);
return failGlyph;
}
private static class MixStatusContextMenu extends ContextMenu {
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(isMixing) {
MenuItem mixStop = new MenuItem("Stop Mixing");
if(pool != null) {
mixStop.disableProperty().bind(pool.mixingProperty().not());
}
mixStop.setGraphic(getStopGlyph());
mixStop.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mixStop(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixStop);
} else {
MenuItem mixNow = new MenuItem("Mix Now");
if(pool != null) {
mixNow.disableProperty().bind(pool.mixingProperty().not());
}
mixNow.setGraphic(getMixGlyph());
mixNow.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mix(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixNow);
}
}
}
}
@@ -20,6 +20,7 @@ import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.*;
import javafx.util.Callback;
import org.controlsfx.control.textfield.AutoCompletionBinding;
@@ -37,6 +38,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
@@ -163,7 +165,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNext(wordEntries.get(i + 1).getEditor());
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
@@ -171,7 +174,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNext(passphraseEntry.getEditor());
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
@@ -259,7 +262,13 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
invalidLabel.setText("Unsupported Electrum seed");
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
} else {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
}
@@ -382,6 +391,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
@@ -392,7 +409,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
@@ -401,7 +418,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private Node next;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
@@ -411,7 +429,29 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField = new TextField() {
@Override
public void paste() {
Clipboard clipboard = Clipboard.getSystemClipboard();
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
String[] words = clipboard.getString().split("[\\n\\s]");
WordEntry entry = WordEntry.this;
for(String word : words) {
if(entry.nextField != null) {
entry.nextField.requestFocus();
}
entry.wordField.setText(word);
entry = entry.nextEntry;
if(entry == null) {
break;
}
}
} else {
super.paste();
}
}
};
wordField.setMaxWidth(100);
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
@@ -431,8 +471,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if (next != null) {
next.requestFocus();
if(nextField != null) {
nextField.requestFocus();
}
});
@@ -454,8 +494,12 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return wordField;
}
public void setNext(Node node) {
next = node;
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
@@ -33,9 +33,9 @@ public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 100;
private static final int MAX_FRAGMENT_LENGTH = 250;
private static final int ANIMATION_PERIOD_MILLIS = 400;
private static final int ANIMATION_PERIOD_MILLIS = 200;
private static final int QR_WIDTH = 480;
private static final int QR_HEIGHT = 480;
@@ -232,7 +232,7 @@ public class QRDisplayDialog extends Dialog<UR> {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -1,9 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamEvent;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
@@ -41,6 +38,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
@@ -74,6 +72,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
@@ -82,7 +82,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
@@ -104,6 +104,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
Platform.runLater(() -> {
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
if(device.getName().equals(Config.get().getWebcamDevice())) {
webcamDeviceProperty.set(device);
}
}
}
});
});
VBox vBox = new VBox(20);
@@ -122,6 +131,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
Config.get().setWebcamDevice(null);
}
final Throwable result = exception;
@@ -134,6 +146,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
webcamService.cancel();
});
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
Config.get().setWebcamDevice(newValue.getName());
if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel();
}
});
setOnCloseRequest(event -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
@@ -146,7 +164,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
AppServices.moveToActiveWindowScreen(this);
@@ -228,7 +247,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//ignore, bytes not parsable as tx
}
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
result = new Result(complete);
}
} else {
PSBT psbt;
@@ -520,6 +539,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
@@ -558,10 +578,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
});
button = hd;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
devicesCombo.setConverter(new StringConverter<>() {
@Override
public String toString(WebcamDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
}
@Override
public WebcamDevice fromString(String string) {
throw new UnsupportedOperationException();
}
});
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
button = devicesCombo;
} else {
button = super.createButton(buttonType);
}
if(button instanceof Region) {
((Region)button).setPrefWidth(150);
((Region)button).setMaxWidth(150);
}
button.disableProperty().bind(webcamService.openingProperty());
return button;
}
@@ -570,7 +612,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -745,6 +787,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
public static class WebcamOpenException extends ScanException {
public WebcamOpenException() {
super();
}
public WebcamOpenException(String message) {
super(message);
}
public WebcamOpenException(Throwable cause) {
super(cause);
}
public WebcamOpenException(String message, Throwable cause) {
super(message, cause);
}
}
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
public long calculateDelay(long snapshotDuration, double deviceFps) {
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.concurrent.Worker;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.controlsfx.dialog.ProgressDialog;
public class ServiceProgressDialog extends ProgressDialog {
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
super(worker);
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle(title);
setHeaderText(header);
dialogPane.getStyleClass().remove("progress-dialog");
Image image = new Image(imagePath);
dialogPane.setGraphic(new ImageView(image));
}
}
@@ -27,7 +27,11 @@ public class TextAreaDialog extends Dialog<String> {
this("");
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
public TextAreaDialog(String defaultValue) {
this(defaultValue, true);
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
final DialogPane dialogPane = new TextAreaDialogPane();
setDialogPane(dialogPane);
@@ -39,6 +43,7 @@ public class TextAreaDialog extends Dialog<String> {
this.textArea.setMaxWidth(Double.MAX_VALUE);
this.textArea.setWrapText(true);
this.textArea.getStyleClass().add("fixed-width");
this.textArea.setEditable(editable);
hbox.getChildren().add(textArea);
HBox.setHgrow(this.textArea, Priority.ALWAYS);
@@ -49,10 +54,13 @@ public class TextAreaDialog extends Dialog<String> {
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialogPane.getButtonTypes().add(ButtonType.OK);
if(editable) {
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(scanButtonType);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(scanButtonType);
}
Platform.runLater(textArea::requestFocus);
@@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.OptionalDouble;
public class TitledDescriptionPane extends TitledPane {
private Label mainLabel;
private Label descriptionLabel;
protected Hyperlink showHideLink;
protected HBox buttonBox;
@@ -50,7 +51,7 @@ public class TitledDescriptionPane extends TitledPane {
VBox labelsBox = new VBox();
labelsBox.setSpacing(5);
labelsBox.setAlignment(Pos.CENTER_LEFT);
Label mainLabel = new Label();
mainLabel = new Label();
mainLabel.setText(title);
mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel);
@@ -99,6 +100,10 @@ public class TitledDescriptionPane extends TitledPane {
return null;
}
public String getTitle() {
return mainLabel.getText();
}
protected void setDescription(String text) {
descriptionLabel.getStyleClass().remove("description-error");
descriptionLabel.getStyleClass().add("description-label");
@@ -0,0 +1,90 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class TorStatusLabel extends Label {
private static final Logger log = LoggerFactory.getLogger(TorStatusLabel.class);
private final TorConnectionTest torConnectionTest = new TorConnectionTest();
public TorStatusLabel() {
getStyleClass().add("tor-status");
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
setGraphic(getIcon());
update();
}
public void update() {
boolean proxyInUse = AppServices.isUsingProxy();
boolean internal = AppServices.isTorRunning();
if(!proxyInUse || internal) {
torConnectionTest.cancel();
if(internal) {
setTooltip(new Tooltip("Internal Tor proxy enabled"));
}
} else if(!torConnectionTest.isRunning()) {
torConnectionTest.setPeriod(Duration.seconds(20.0));
torConnectionTest.setBackoffStrategy(null);
torConnectionTest.setOnSucceeded(workerStateEvent -> {
getStyleClass().remove("failure");
setTooltip(new Tooltip("External Tor proxy enabled"));
});
torConnectionTest.setOnFailed(workerStateEvent -> {
if(!getStyleClass().contains("failure")) {
getStyleClass().add("failure");
}
setTooltip(new Tooltip("External Tor proxy error: " + workerStateEvent.getSource().getException().getMessage()));
log.warn("Failed to connect to external Tor proxy: " + workerStateEvent.getSource().getException().getMessage());
});
torConnectionTest.start();
}
}
private Node getIcon() {
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
adjust.setRotate(180);
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
bullseye.setFontSize(14);
Group group = new Group();
group.getChildren().addAll(adjust, bullseye);
return group;
}
private static class TorConnectionTest extends ScheduledService<Void> {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() throws IOException {
HostAndPort proxyHostAndPort = HostAndPort.fromString(Config.get().getProxyServer());
Socket socket = new Socket(proxyHostAndPort.getHost(), proxyHostAndPort.getPort());
socket.close();
return null;
}
};
}
}
}
@@ -162,28 +162,28 @@ public class TransactionDiagram extends GridPane {
topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(0);
topYaxis.setEndY(10);
topYaxis.getStyleClass().add("inputs-type");
Line topBracket = new Line();
topBracket.setStartX(width * 0.5);
topBracket.setStartY(0);
topBracket.setStartY(10);
topBracket.setEndX(width);
topBracket.setEndY(0);
topBracket.setEndY(10);
topBracket.getStyleClass().add("inputs-type");
Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(getDiagramHeight());
bottomYaxis.setStartY(getDiagramHeight() - 10);
bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(getDiagramHeight());
bottomBracket.setStartY(getDiagramHeight() - 10);
bottomBracket.setEndX(width);
bottomBracket.setEndY(getDiagramHeight());
bottomBracket.setEndY(getDiagramHeight() - 10);
bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
@@ -344,7 +344,7 @@ public class TransactionDiagram extends GridPane {
group.getChildren().add(yaxisLine);
double width = 140.0;
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
@@ -377,12 +377,13 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(createSpacer());
for(Payment payment : displayedPayments) {
boolean isConsolidation = walletTx.isConsolidationSend(payment);
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
Glyph outputGlyph = getOutputGlyph(payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip);
@@ -390,15 +391,16 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(createSpacer());
}
if(walletTx.getChangeNode() != null) {
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
WalletNode changeNode = changeEntry.getKey();
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
boolean overGapLimit = (walletTx.getChangeNode().getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox();
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip);
@@ -467,6 +469,24 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
public Glyph getOutputGlyph(Payment payment) {
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);
}
return getPaymentGlyph();
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
@@ -488,6 +508,34 @@ public class TransactionDiagram extends GridPane {
return consolidationGlyph;
}
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();
}
@@ -578,6 +626,20 @@ public class TransactionDiagram extends GridPane {
this.additionalPayments = additionalPayments;
}
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
Glyph glyph = null;
for(Payment payment : additionalPayments) {
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
return getPaymentGlyph();
}
glyph = paymentGlyph;
}
return glyph;
}
public String toString() {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
}
@@ -4,7 +4,12 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.stage.Popup;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.event.MouseOverTextEvent;
@@ -21,6 +26,7 @@ public class TransactionHexArea extends CodeArea {
private static final int TRUNCATE_AT = 30000;
private static final int SEGMENTS_INTERVAL = 250;
private String fullHex;
private List<TransactionSegment> previousSegmentList = new ArrayList<>();
public TransactionHexArea() {
@@ -33,7 +39,8 @@ public class TransactionHexArea extends CodeArea {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
transaction.bitcoinSerializeToStream(baos);
String hex = Utils.bytesToHex(baos.toByteArray());
fullHex = Utils.bytesToHex(baos.toByteArray());
String hex = fullHex;
if(hex.length() > TRUNCATE_AT) {
hex = hex.substring(0, TRUNCATE_AT);
hex += "[truncated]";
@@ -42,6 +49,7 @@ public class TransactionHexArea extends CodeArea {
clear();
appendText(hex);
previousSegmentList = new ArrayList<>();
setContextMenu(new TransactionHexContextMenu(fullHex));
} catch (IOException e) {
throw new IllegalStateException("Can't happen");
}
@@ -236,6 +244,18 @@ public class TransactionHexArea extends CodeArea {
};
}
@Override
public void copy() {
IndexRange selection = getSelection();
if(fullHex != null && selection.getLength() == getLength()) {
ClipboardContent content = new ClipboardContent();
content.putString(fullHex);
Clipboard.getSystemClipboard().setContent(content);
} else {
super.copy();
}
}
private static class TransactionSegment {
public TransactionSegment(int start, int length, Integer index, Integer witnessIndex, String style) {
this.start = start;
@@ -266,4 +286,18 @@ public class TransactionHexArea extends CodeArea {
return Objects.hash(start, length, style);
}
}
private static class TransactionHexContextMenu extends ContextMenu {
public TransactionHexContextMenu(String hex) {
MenuItem copy = new MenuItem("Copy All");
copy.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hex);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copy);
}
}
}
@@ -74,7 +74,7 @@ public class UtxosChart extends BarChart<String, Number> {
private String getCategoryName(Entry entry) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
}
return ((UtxoEntry)entry).getDescription();
@@ -7,6 +7,7 @@ import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator;
import java.util.List;
public class UtxosTreeTable extends CoinTreeTable {
@@ -38,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
});
getColumns().add(outputCol);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
});
getColumns().add(addressCol);
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
});
mixStatusCol.setCellFactory(p -> new MixStatusCell());
mixStatusCol.setSortable(true);
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
getColumns().add(mixStatusCol);
} else {
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
getColumns().add(addressCol);
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
@@ -93,7 +101,9 @@ public class UtxosTreeTable extends CoinTreeTable {
public void updateHistory(List<WalletNode> updatedNodes) {
//Utxo entries should have already been updated, so only a resort required
sort();
if(!getRoot().getChildren().isEmpty()) {
sort();
}
}
public void updateLabel(Entry entry) {
@@ -11,6 +11,7 @@ import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.Comparator;
import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> {
@@ -43,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -53,6 +54,8 @@ public class WalletExportDialog extends Dialog<Wallet> {
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
exportAccordion.getPanes().add(exportPane);
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(exportAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
@@ -16,6 +16,7 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import java.util.Comparator;
import java.util.List;
public class WalletImportDialog extends Dialog<Wallet> {
@@ -47,17 +48,19 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
@@ -92,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
List<Device> devices = enumerateService.getValue();
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
for(Device device : devices) {
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1);
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null);
importAccordion.getPanes().add(0, devicePane);
}
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
@@ -53,6 +53,10 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
final VBox content = new VBox(20);
name = (CustomTextField)TextFields.createClearableTextField();
name.setText(initialName);
name.setTextFormatter(new TextFormatter<>((change) -> {
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
return change;
}));
content.getChildren().add(name);
HBox existingBox = new HBox(10);
@@ -131,10 +131,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
LOAD("Enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
UPDATE_SET("Re-enter the wallet password:", "Verify Password"),
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
private final String description;
@@ -14,6 +14,7 @@ import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -312,6 +313,23 @@ public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess
return this.fps;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
WebcamScanDevice that = (WebcamScanDevice) o;
return Objects.equals(fullname, that.fullname);
}
@Override
public int hashCode() {
return Objects.hash(fullname);
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
@@ -3,20 +3,43 @@ package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
private static boolean rescan;
@Override
public List<WebcamDevice> getDevices() {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
if(rescan || webcamDevices.isEmpty()) {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
scanDevices.add(scanDevice);
}
}
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
newDevices.removeAll(webcamDevices);
webcamDevices.addAll(newDevices);
webcamDevices.removeIf(device -> !scanDevices.contains(device));
}
return scanDevices;
return webcamDevices;
}
public static ObservableList<WebcamDevice> getFoundDevices() {
return webcamDevices;
}
public static void rescan() {
rescan = true;
}
}
@@ -1,12 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
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.sparrow.io.Config;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -23,26 +22,30 @@ import java.util.concurrent.TimeUnit;
public class WebcamService extends ScheduledService<Image> {
private WebcamResolution resolution;
private WebcamDevice device;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
private Webcam cam;
private long lastQrSampleTime;
private final Reader qrReader;
static {
Webcam.setDriver(new WebcamScanDriver());
}
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution;
this.device = device;
this.listener = listener;
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader();
}
@Override
@@ -58,6 +61,23 @@ public class WebcamService extends ScheduledService<Image> {
}
cam = webcams.get(0);
if(device != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(device.getName())) {
cam = webcam;
}
}
} else if(Config.get().getWebcamDevice() != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
cam = webcam;
}
}
}
device = cam.getDevice();
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
@@ -70,6 +90,10 @@ public class WebcamService extends ScheduledService<Image> {
}
BufferedImage bimg = cam.getImage();
if(bimg == null) {
return null;
}
Image image = SwingFXUtils.toFXImage(bimg, null);
updateValue(image);
@@ -106,9 +130,9 @@ public class WebcamService extends ScheduledService<Image> {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
Result result = qrReader.decode(bitmap);
resultProperty.set(result);
} catch(NotFoundException e) {
} catch(ReaderException e) {
// fall thru, it means there is no QR code in image
}
}
@@ -133,6 +157,14 @@ public class WebcamService extends ScheduledService<Image> {
this.resolution = resolution;
}
public WebcamDevice getDevice() {
return device;
}
public void setDevice(WebcamDevice device) {
this.device = device;
}
public boolean isOpening() {
return opening.get();
}
@@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class ChildWalletAddedEvent extends WalletChangedEvent {
private final Storage storage;
private final Wallet childWallet;
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
super(masterWallet);
this.storage = storage;
this.childWallet = childWallet;
}
public Storage getStorage() {
return storage;
}
public Wallet getChildWallet() {
return childWallet;
}
public String getMasterWalletId() {
return storage.getWalletId(getWallet());
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class MixToConfigChangedEvent {
private final Wallet wallet;
public MixToConfigChangedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -6,21 +6,25 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class NewWalletTransactionsEvent {
private final Wallet wallet;
private final List<BlockTransaction> blockTransactions;
private final List<TransactionEntry> transactionEntries;
private final long totalBlockchainValue;
private final long totalMempoolValue;
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
public NewWalletTransactionsEvent(Wallet wallet, List<TransactionEntry> transactionEntries) {
this.wallet = wallet;
this.blockTransactions = blockTransactions;
this.totalBlockchainValue = totalBlockchainValue;
this.totalMempoolValue = totalMempoolValue;
this.transactionEntries = transactionEntries;
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
}
public Wallet getWallet() {
@@ -28,7 +32,7 @@ public class NewWalletTransactionsEvent {
}
public List<BlockTransaction> getBlockTransactions() {
return blockTransactions;
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
public long getTotalValue() {
@@ -55,4 +59,13 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -12,6 +13,7 @@ public class SpendUtxoEvent {
private final List<Payment> payments;
private final Long fee;
private final boolean includeSpentMempoolOutputs;
private final Pool pool;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, false);
@@ -23,6 +25,16 @@ public class SpendUtxoEvent {
this.payments = payments;
this.fee = fee;
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
this.pool = null;
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.fee = fee;
this.includeSpentMempoolOutputs = false;
this.pool = pool;
}
public Wallet getWallet() {
@@ -44,4 +56,8 @@ public class SpendUtxoEvent {
public boolean isIncludeSpentMempoolOutputs() {
return includeSpentMempoolOutputs;
}
public Pool getPool() {
return pool;
}
}
@@ -1,12 +1,16 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.io.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class StorageEvent extends TimedEvent {
private static final Logger log = LoggerFactory.getLogger(StorageEvent.class);
private static boolean firstRunDone = false;
private static final Map<String, Long> eventTime = new HashMap<>();
@@ -22,8 +26,10 @@ public class StorageEvent extends TimedEvent {
eventTime.put(walletId, System.currentTimeMillis());
timeMills = keyDerivationPeriod;
} else if(action == Action.END) {
long start = eventTime.get(walletId);
if(firstRunDone) {
Long start = eventTime.get(walletId);
if(start == null) {
log.error("Could not find start event time for wallet id " + walletId);
} else if(firstRunDone) {
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLockEvent {
private final Wallet wallet;
public WalletLockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMasterMixConfigChangedEvent extends WalletMixConfigChangedEvent {
public WalletMasterMixConfigChangedEvent(Wallet wallet) {
super(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMixConfigChangedEvent extends WalletChangedEvent {
public WalletMixConfigChangedEvent(Wallet wallet) {
super(wallet);
}
}
@@ -19,8 +19,7 @@ public class WalletNodeHistoryChangedEvent {
}
public WalletNode getWalletNode(Wallet wallet) {
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
for(KeyPurpose keyPurpose : keyPurposes) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
if(changedNode != null) {
return changedNode;
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class WalletOpenedEvent {
private final Storage storage;
private final Wallet wallet;
public WalletOpenedEvent(Storage storage, Wallet wallet) {
this.storage = storage;
this.wallet = wallet;
}
public Storage getStorage() {
return storage;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.WalletTabData;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
public class WalletTabSelectedEvent extends TabSelectedEvent {
public WalletTabSelectedEvent(Tab tab) {
@@ -9,6 +10,8 @@ public class WalletTabSelectedEvent extends TabSelectedEvent {
}
public WalletTabData getWalletTabData() {
return (WalletTabData)getTabData();
TabPane subTabs = (TabPane)getTab().getContent();
Tab subTab = subTabs.getSelectionModel().getSelectedItem();
return (WalletTabData)subTab.getUserData();
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletUnlockEvent {
private final Wallet wallet;
public WalletUnlockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.Map;
public class WalletUtxoMixesChangedEvent extends WalletChangedEvent {
private final Map<Sha256Hash, UtxoMixData> changedUtxoMixes;
private final Map<Sha256Hash, UtxoMixData> removedUtxoMixes;
public WalletUtxoMixesChangedEvent(Wallet wallet, Map<Sha256Hash, UtxoMixData> changedUtxoMixes, Map<Sha256Hash, UtxoMixData> removedUtxoMixes) {
super(wallet);
this.changedUtxoMixes = changedUtxoMixes;
this.removedUtxoMixes = removedUtxoMixes;
}
public Map<Sha256Hash, UtxoMixData> getChangedUtxoMixes() {
return changedUtxoMixes;
}
public Map<Sha256Hash, UtxoMixData> getRemovedUtxoMixes() {
return removedUtxoMixes;
}
}
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
private final BlockTransactionHashIndex utxo;
import java.util.List;
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
private final List<BlockTransactionHashIndex> utxos;
public WalletUtxoStatusChangedEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
super(wallet);
this.utxo = utxo;
this.utxos = utxos;
}
public BlockTransactionHashIndex getUtxo() {
return utxo;
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
}
}
@@ -0,0 +1,67 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WhirlpoolMixEvent {
private final Wallet wallet;
private final BlockTransactionHashIndex utxo;
private final MixProgress mixProgress;
private final Utxo nextUtxo;
private final MixFailReason mixFailReason;
private final String mixError;
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = mixProgress;
this.nextUtxo = null;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = nextUtxo;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = null;
this.mixFailReason = mixFailReason;
this.mixError = mixError;
}
public Wallet getWallet() {
return wallet;
}
public BlockTransactionHashIndex getUtxo() {
return utxo;
}
public MixProgress getMixProgress() {
return mixProgress;
}
public Utxo getNextUtxo() {
return nextUtxo;
}
public MixFailReason getMixFailReason() {
return mixFailReason;
}
public String getMixError() {
return mixError;
}
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
private final WalletNode walletNode;
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
super(wallet, utxo, nextUtxo);
this.walletNode = walletNode;
}
public WalletNode getWalletNode() {
return walletNode;
}
}
@@ -15,11 +15,15 @@ public class FontAwesome5 extends GlyphFont {
* The individual glyphs offered by the FontAwesome5 font.
*/
public static enum Glyph implements INamedCharacter {
ADJUST('\uf042'),
ARROW_CIRCLE_DOWN('\uf0ab'),
ANGLE_DOUBLE_RIGHT('\uf101'),
ARROW_DOWN('\uf063'),
ARROW_UP('\uf062'),
BAN('\uf05e'),
BIOHAZARD('\uf780'),
BTC('\uf15a'),
BULLSEYE('\uf140'),
CAMERA('\uf030'),
CHECK_CIRCLE('\uf058'),
CIRCLE('\uf111'),
@@ -31,17 +35,23 @@ public class FontAwesome5 extends GlyphFont {
EXTERNAL_LINK_ALT('\uf35d'),
ELLIPSIS_H('\uf141'),
EYE('\uf06e'),
FEATHER_ALT('\uf56b'),
FILE_CSV('\uf6dd'),
HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'),
HAND_HOLDING_WATER('\uf4c1'),
HISTORY('\uf1da'),
INFO_CIRCLE('\uf05a'),
KEY('\uf084'),
LAPTOP('\uf109'),
LOCK('\uf023'),
LOCK_OPEN('\uf3c1'),
MINUS_CIRCLE('\uf056'),
PEN_FANCY('\uf5ac'),
PLUS('\uf067'),
PLAY_CIRCLE('\uf144'),
PLUS_CIRCLE('\uf055'),
STOP_CIRCLE('\uf28d'),
QRCODE('\uf029'),
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),
@@ -52,14 +62,17 @@ public class FontAwesome5 extends GlyphFont {
SIGN_OUT_ALT('\uf2f5'),
SQUARE('\uf0c8'),
SNOWFLAKE('\uf2dc'),
SORT_NUMERIC_DOWN('\uf162'),
SUN('\uf185'),
THEATER_MASKS('\uf630'),
TIMES_CIRCLE('\uf057'),
TOGGLE_OFF('\uf204'),
TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'),
UNDO('\uf0e2'),
USER_FRIENDS('\uf500'),
WALLET('\uf555');
WALLET('\uf555'),
WEIGHT('\uf496');
private final char ch;
@@ -1,10 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import java.util.List;
@@ -31,6 +28,15 @@ public class Bip39 implements KeystoreMnemonicImport {
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
return Keystore.fromSeed(seed, derivation);
} catch (Exception e) {
try {
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);
throw new ImportException(new MnemonicException.MnemonicTypeException(DeterministicSeed.Type.ELECTRUM));
} catch(Exception ex) {
if(ex instanceof ImportException && ex.getCause() instanceof MnemonicException.MnemonicTypeException) {
throw (ImportException)ex;
}
}
throw new ImportException(e);
}
}
@@ -0,0 +1,185 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
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.drongo.wallet.WalletModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class CaravanMultisig implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(ColdcardMultisig.class);
@Override
public String getName() {
return "Caravan Multisig";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.CARAVAN;
}
@Override
public String getWalletImportDescription() {
return "Import the file created via the Download Wallet Details button in Caravan.";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
try {
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
CaravanFile cf = JsonPersistence.getGson().fromJson(reader, CaravanFile.class);
Wallet wallet = new Wallet();
wallet.setName(cf.name);
wallet.setPolicyType(PolicyType.MULTI);
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
Keystore keystore = new Keystore(extKey.name);
try {
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
} catch(NumberFormatException e) {
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, scriptType.getDefaultDerivationPath()));
}
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(extKey.xpub));
WalletModel walletModel = WalletModel.fromType(extKey.method);
if(walletModel == null) {
keystore.setWalletModel(WalletModel.SPARROW);
keystore.setSource(KeystoreSource.SW_WATCH);
} else {
keystore.setWalletModel(walletModel);
keystore.setSource(KeystoreSource.HW_USB);
}
wallet.getKeystores().add(keystore);
}
wallet.setScriptType(scriptType);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
return wallet;
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}
@Override
public boolean isWalletImportScannable() {
return false;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
if(!wallet.isValid()) {
throw new ExportException("Cannot export an incomplete wallet");
}
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}
try {
CaravanFile cf = new CaravanFile();
cf.name = wallet.getFullName();
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
cf.network = Network.get().getName();
cf.client = new Client();
Quorum quorum = new Quorum();
quorum.requiredSigners = wallet.getDefaultPolicy().getNumSignaturesRequired();
quorum.totalSigners = wallet.getKeystores().size();
cf.quorum = quorum;
cf.extendedPublicKeys = new ArrayList<>();
for(Keystore keystore : wallet.getKeystores()) {
ExtPublicKey extKey = new ExtPublicKey();
extKey.name = keystore.getLabel();
extKey.bip32Path = keystore.getKeyDerivation().getDerivationPath();
extKey.xpub = keystore.getExtendedPublicKey().toString();
extKey.xfp = keystore.getKeyDerivation().getMasterFingerprint();
extKey.method = keystore.getWalletModel().getType();
cf.extendedPublicKeys.add(extKey);
}
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(cf);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch(Exception e) {
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}
@Override
public String getWalletExportDescription() {
return "Export a file that can be imported via the Import Wallet Configuration button in Caravan.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "json";
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
@Override
public boolean isEncrypted(File file) {
return false;
}
private static final class CaravanFile {
public String name;
public String addressType;
public String network;
public Client client;
public Quorum quorum;
public List<ExtPublicKey> extendedPublicKeys;
public int startingAddressIndex = 0;
}
private static final class Client {
public String type = "public";
}
private static final class Quorum {
public int requiredSigners;
public int totalSigners;
}
private static final class ExtPublicKey {
public String name;
public String bip32Path;
public String xpub;
public String xfp;
public String method;
}
}
@@ -57,8 +57,8 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
return keystore;
} catch (Exception e) {
log.error("Error getting Cobo Vault keystore", e);
throw new ImportException("Error getting Cobo Vault keystore", e);
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -81,7 +81,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
try {
wallet.checkWallet();
} catch(InvalidWalletException e) {
throw new ImportException("Imported Cobo Vault wallet was invalid: " + e.getMessage());
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
}
return wallet;
@@ -42,23 +42,27 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COLDCARD);
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
try {
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
} catch(NullPointerException e) {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
@@ -150,8 +154,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
return wallet;
} catch(Exception e) {
log.error("Error importing Coldcard multisig wallet", e);
throw new ImportException("Error importing Coldcard multisig wallet", e);
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}
@@ -183,7 +187,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.append("# " + getName() + " setup file (created by Sparrow)\n");
writer.append("#\n");
writer.append("Name: ").append(wallet.getName()).append("\n");
writer.append("Name: ").append(wallet.getFullName()).append("\n");
writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
if(!multipleDerivations) {
writer.append("Derivation: ").append(wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()).append("\n");
@@ -203,8 +207,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
writer.flush();
} catch(Exception e) {
log.error("Error exporting Coldcard multisig wallet", e);
throw new ExportException("Error exporting Coldcard multisig wallet", e);
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}
@@ -86,8 +86,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
}
} catch (Exception e) {
log.error("Error getting Coldcard keystore", e);
throw new ImportException("Error getting Coldcard keystore", e);
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
throw new ImportException("Correct derivation not found for script type: " + scriptType);
@@ -6,13 +6,12 @@ import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
@@ -28,6 +27,7 @@ public class Config {
private BitcoinUnit bitcoinUnit;
private FeeRatesSource feeRatesSource;
private FeeRatesSelection feeRatesSelection;
private OptimizationStrategy sendOptimizationStrategy;
private Currency fiatCurrency;
private ExchangeSource exchangeSource;
private boolean loadRecentWallets = true;
@@ -42,10 +42,12 @@ public class Config {
private boolean showTransactionHex = true;
private boolean showLoadingLog = false;
private boolean showUtxosChart = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private File hwi;
private Boolean hdCapture;
private String webcamDevice;
private ServerType serverType;
private String publicElectrumServer;
private String coreServer;
@@ -58,6 +60,8 @@ public class Config {
private File electrumServerCert;
private boolean useProxy;
private String proxyServer;
private Double appWidth;
private Double appHeight;
private static Config INSTANCE;
@@ -137,6 +141,15 @@ public class Config {
flush();
}
public OptimizationStrategy getSendOptimizationStrategy() {
return sendOptimizationStrategy;
}
public void setSendOptimizationStrategy(OptimizationStrategy sendOptimizationStrategy) {
this.sendOptimizationStrategy = sendOptimizationStrategy;
flush();
}
public Currency getFiatCurrency() {
return fiatCurrency;
}
@@ -267,6 +280,14 @@ public class Config {
flush();
}
public boolean isPreventSleep() {
return preventSleep;
}
public void setPreventSleep(boolean preventSleep) {
this.preventSleep = preventSleep;
}
public List<File> getRecentWalletFiles() {
return recentWalletFiles;
}
@@ -307,6 +328,15 @@ public class Config {
flush();
}
public String getWebcamDevice() {
return webcamDevice;
}
public void setWebcamDevice(String webcamDevice) {
this.webcamDevice = webcamDevice;
flush();
}
public ServerType getServerType() {
return serverType;
}
@@ -395,24 +425,6 @@ public class Config {
flush();
}
public Boolean getCoreMultiWallet() {
return coreMultiWallet;
}
public void setCoreMultiWallet(Boolean coreMultiWallet) {
this.coreMultiWallet = coreMultiWallet;
flush();
}
public String getCoreWallet() {
return coreWallet;
}
public void setCoreWallet(String coreWallet) {
this.coreWallet = coreWallet;
flush();
}
public String getElectrumServer() {
return electrumServer;
}
@@ -449,6 +461,24 @@ public class Config {
flush();
}
public Double getAppWidth() {
return appWidth;
}
public void setAppWidth(Double appWidth) {
this.appWidth = appWidth;
flush();
}
public Double getAppHeight() {
return appHeight;
}
public void setAppHeight(Double appHeight) {
this.appHeight = appHeight;
flush();
}
private synchronized void flush() {
Gson gson = getGson();
try {
@@ -119,8 +119,8 @@ public class Hwi {
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
try {
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
}
String descriptor = outputDescriptor.toString();
@@ -254,6 +254,7 @@ public class Hwi {
File hwiHomeDir = getHwiHomeDir();
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
IOUtils.deleteDirectory(hwiVersionDir);
if(!hwiVersionDir.exists()) {
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
@@ -90,7 +90,9 @@ public class IOUtils {
// if it is a subdirectory, we just return the directory name
entry = entry.substring(0, checkSubdir);
}
result.add(entry);
if(!entry.isEmpty()) {
result.add(entry);
}
}
}
@@ -43,8 +43,8 @@ public class JsonPersistence implements Persistence {
wallet = gson.fromJson(reader, Wallet.class);
}
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
@@ -68,8 +68,8 @@ public class JsonPersistence implements Persistence {
wallet = gson.fromJson(reader, Wallet.class);
}
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
@@ -77,16 +77,16 @@ public class JsonPersistence implements Persistence {
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
}
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
for(File childFile : walletFiles) {
Wallet childWallet = loadWallet(childFile, encryptionKey);
Storage childStorage = new Storage(childFile);
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
childStorage.setKeyDeriver(getKeyDeriver());
childWallet.setMasterWallet(masterWallet);
childWallets.put(childStorage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
childWallets.put(new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;
@@ -200,6 +200,11 @@ public class JsonPersistence implements Persistence {
return "BIE1".getBytes(StandardCharsets.UTF_8);
}
@Override
public boolean isPersisted(Storage storage, Wallet wallet) {
return storage.getWalletFile().exists();
}
@Override
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
return getEncryptionKey(password, null, null);
@@ -62,11 +62,11 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
return keystore;
} catch (IllegalArgumentException e) {
log.error("Error getting Keystone keystore - not an output descriptor");
throw new ImportException("Error getting Keystone keystore", e);
log.error("Error getting " + getName() + " keystore - not an output descriptor");
throw new ImportException("Error getting " + getName() + " keystore", e);
} catch (Exception e) {
log.error("Error getting Keystone keystore", e);
throw new ImportException("Error getting Keystone keystore", e);
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -89,7 +89,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
try {
wallet.checkWallet();
} catch(InvalidWalletException e) {
throw new ImportException("Imported Keystone wallet was invalid: " + e.getMessage());
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
}
return wallet;
@@ -16,6 +16,7 @@ public interface Persistence {
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
void updateWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
boolean isPersisted(Storage storage, Wallet wallet);
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
AsymmetricKeyDeriver getKeyDeriver();
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
@@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.WalletModel;
public class SeedSigner extends SpecterDIY {
@Override
public String getName() {
return "SeedSigner";
}
@Override
public String getKeystoreImportDescription() {
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports multisig wallets with a P2WSH script type.";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SEEDSIGNER;
}
}
@@ -12,6 +12,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.StandardCopyOption;
import java.util.Map;
public class Sparrow implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(Sparrow.class);
@@ -29,12 +30,20 @@ public class Sparrow implements WalletImport, WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
File tempFile = File.createTempFile(wallet.getName(), null);
Storage tempStorage = new Storage(PersistenceType.JSON, tempFile);
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
PersistenceType persistenceType = exportedWallet.getChildWallets().isEmpty() ? PersistenceType.JSON : PersistenceType.DB;
Persistence persistence = persistenceType.getInstance();
Storage storage = AppServices.get().getOpenWallets().get(exportedWallet);
File tempFile = File.createTempFile(exportedWallet.getName(), "." + persistenceType.getExtension());
tempFile.delete();
Storage tempStorage = new Storage(persistence, tempFile);
tempStorage.setKeyDeriver(storage.getKeyDeriver());
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
tempStorage.saveWallet(wallet);
tempStorage.saveWallet(exportedWallet);
for(Wallet childWallet : exportedWallet.getChildWallets()) {
tempStorage.saveWallet(childWallet);
}
persistence.close();
Files.copy(tempStorage.getWalletFile(), outputStream);
outputStream.flush();
tempStorage.getWalletFile().delete();
@@ -46,14 +55,15 @@ public class Sparrow implements WalletImport, WalletExport {
@Override
public String getWalletExportDescription() {
return "Exports your Sparrow wallet file, which can be imported into another Sparrow instance running on any supported platform.";
return "Exports your Sparrow wallet file, which can be imported into another Sparrow instance running on any supported platform. If the wallet is encrypted, the same password is used to encrypt the exported file.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
try {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
return storage.isEncrypted() ? "" : PersistenceType.JSON.getExtension();
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
return !exportedWallet.getChildWallets().isEmpty() ? PersistenceType.DB.getExtension() : (storage.isEncrypted() ? "" : PersistenceType.JSON.getExtension());
} catch(IOException e) {
//ignore
}
@@ -83,21 +93,46 @@ public class Sparrow implements WalletImport, WalletExport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Storage storage = null;
Wallet wallet = null;
File tempFile = null;
try {
tempFile = File.createTempFile("sparrow", null);
java.nio.file.Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
Storage storage = new Storage(PersistenceType.JSON, tempFile);
PersistenceType persistenceType = Storage.detectPersistenceType(tempFile);
persistenceType = (persistenceType == null ? PersistenceType.JSON : persistenceType);
File tempTypedFile = new File(tempFile.getParentFile(), tempFile.getName() + "." + persistenceType.getExtension());
tempFile.renameTo(tempTypedFile);
tempFile = tempTypedFile;
storage = new Storage(persistenceType, tempFile);
if(!isEncrypted(tempFile)) {
return storage.loadUnencryptedWallet().getWallet();
wallet = storage.loadUnencryptedWallet().getWallet();
} else {
return storage.loadEncryptedWallet(password).getWallet();
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
wallet = walletBackupAndKey.getWallet();
wallet.decrypt(walletBackupAndKey.getKey());
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
entry.getKey().getWallet().decrypt(entry.getKey().getKey());
}
}
return wallet;
} catch(IOException | StorageException e) {
log.error("Error importing Sparrow wallet", e);
throw new ImportException("Error importing Sparrow wallet", e);
} finally {
if(storage != null) {
storage.close();
}
if(tempFile != null) {
if(wallet != null) {
File migratedWalletFile = Storage.getExistingWallet(tempFile.getParentFile(), wallet.getName());
if(migratedWalletFile != null) {
migratedWalletFile.delete();
}
}
tempFile.delete();
}
}
@@ -30,13 +30,13 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(getName());
keystore.setWalletModel(WalletModel.SPECTER_DIY);
keystore.setWalletModel(getWalletModel());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
return keystore;
} catch(IOException e) {
log.error("Error getting Specter DIY keystore", e);
throw new ImportException("Error getting Specter DIY keystore", e);
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -69,11 +69,11 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.append("addwallet ").append(wallet.getName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
writer.flush();
} catch(Exception e) {
log.error("Error exporting Specter DIY wallet", e);
throw new ExportException("Error exporting Specter DIY wallet", e);
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,6 +12,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class SpecterDesktop implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
@@ -23,7 +21,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getName();
specterWallet.label = wallet.getFullName();
specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight());
specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
@@ -32,8 +30,8 @@ public class SpecterDesktop implements WalletImport, WalletExport {
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch(Exception e) {
log.error("Error exporting Specter Desktop wallet", e);
throw new ExportException("Error exporting Specter Desktop wallet", e);
log.error("Error exporting " + getName() + " wallet", e);
throw new ExportException("Error exporting " + getName() + " wallet", e);
}
}
@@ -63,20 +61,43 @@ public class SpecterDesktop implements WalletImport, WalletExport {
Wallet wallet = outputDescriptor.toWallet();
wallet.setName(specterWallet.label);
if(specterWallet.devices != null && specterWallet.devices.size() == wallet.getKeystores().size()) {
boolean uniqueLabels = specterWallet.devices.stream().map(SpecterWalletDevice::getLabel).distinct().count() == specterWallet.devices.size();
for(int i = 0; i < specterWallet.devices.size(); i++) {
SpecterWalletDevice device = specterWallet.devices.get(i);
Keystore keystore = wallet.getKeystores().get(i);
keystore.setLabel(device.getLabel() + (uniqueLabels ? "" : " " + i));
WalletModel walletModel = device.getWalletModel();
if(walletModel != null) {
keystore.setWalletModel(walletModel);
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.KEEPKEY ||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X ||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
keystore.setSource(KeystoreSource.HW_USB);
} else if(walletModel == WalletModel.BITCOIN_CORE) {
keystore.setSource(KeystoreSource.SW_WATCH);
} else {
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
}
}
}
}
try {
wallet.checkWallet();
} catch(InvalidWalletException e) {
throw new ImportException("Imported Specter wallet was invalid: " + e.getMessage());
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
}
return wallet;
}
} catch(Exception e) {
log.error("Error importing Specter Desktop wallet", e);
throw new ImportException("Error importing Specter Desktop wallet", e);
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
throw new ImportException("File was not a valid Specter Desktop wallet");
throw new ImportException("File was not a valid " + getName() + " wallet");
}
@Override
@@ -113,5 +134,40 @@ public class SpecterDesktop implements WalletImport, WalletExport {
public String label;
public Integer blockheight;
public String descriptor;
public List<SpecterWalletDevice> devices;
}
public static class SpecterWalletDevice {
public String type;
public String label;
public WalletModel getWalletModel() {
if(type != null) {
String model = type;
if(model.equals("cobo")) {
model = "cobovault";
}
WalletModel walletModel = WalletModel.fromType(model);
if(walletModel == WalletModel.SPECTER_DESKTOP) {
walletModel = WalletModel.SPECTER_DIY;
}
if(walletModel == WalletModel.TREZOR_1) {
walletModel = WalletModel.TREZOR_T;
}
return walletModel;
}
return null;
}
public String getLabel() {
if(label == null) {
label = "Keystore";
}
return label.length() > Keystore.MAX_LABEL_LENGTH - 3 ? label.substring(0, Keystore.MAX_LABEL_LENGTH - 3) : label;
}
}
}
@@ -62,6 +62,10 @@ public class Storage {
}
public boolean isEncrypted() throws IOException {
if(!walletFile.exists()) {
return false;
}
return persistence.isEncrypted(walletFile);
}
@@ -115,6 +119,10 @@ public class Storage {
}
}
public boolean isPersisted(Wallet wallet) {
return persistence.isPersisted(this, wallet);
}
public void close() {
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
closePersistenceService.start();
@@ -165,6 +173,20 @@ public class Storage {
}
public void deleteTempBackups() {
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
if(backups.length > 0) {
try {
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(backups[0].getName()));
ProcessHandle.Info processInfo = ProcessHandle.current().info();
if(processInfo.startInstant().isPresent() && processInfo.startInstant().get().isAfter(date.toInstant())) {
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
backups[0].renameTo(permanent);
}
} catch(Exception e) {
log.error("Error copying temporary to permanent backup", e);
}
}
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
}
@@ -289,13 +311,17 @@ public class Storage {
}
public static File getExistingWallet(String walletName) {
File encrypted = new File(getWalletsDir(), walletName.trim());
return getExistingWallet(getWalletsDir(), walletName);
}
public static File getExistingWallet(File walletsDir, String walletName) {
File encrypted = new File(walletsDir, walletName.trim());
if(encrypted.exists()) {
return encrypted;
}
for(PersistenceType persistenceType : PersistenceType.values()) {
File unencrypted = new File(getWalletsDir(), walletName.trim() + "." + persistenceType.getExtension());
File unencrypted = new File(walletsDir, walletName.trim() + "." + persistenceType.getExtension());
if(unencrypted.exists()) {
return unencrypted;
}
@@ -334,6 +360,11 @@ public class Storage {
return type.getInstance().isEncrypted(walletFile);
}
}
PersistenceType detectedType = detectPersistenceType(walletFile);
if(detectedType != null) {
return detectedType.getInstance().isEncrypted(walletFile);
}
} catch(IOException e) {
//ignore
}
@@ -341,6 +372,24 @@ public class Storage {
return FileType.BINARY.equals(IOUtils.getFileType(walletFile));
}
public static PersistenceType detectPersistenceType(File walletFile) {
try(Reader reader = new FileReader(walletFile)) {
int firstChar = reader.read();
if(firstChar == 'U' || firstChar == '{') {
return PersistenceType.JSON;
}
if(firstChar == 'H') {
return PersistenceType.DB;
}
} catch(IOException e) {
log.error("Error detecting persistence type", e);
}
return null;
}
public static File getWalletsBackupDir() {
File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR);
if(!walletsBackupDir.exists()) {
@@ -500,10 +549,18 @@ public class Storage {
public static class KeyDerivationService extends Service<ECKey> {
private final Storage storage;
private final SecureString password;
private final boolean verifyPassword;
public KeyDerivationService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
this.verifyPassword = false;
}
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
this.storage = storage;
this.password = password;
this.verifyPassword = verifyPassword;
}
@Override
@@ -511,7 +568,12 @@ public class Storage {
return new Task<>() {
protected ECKey call() throws IOException, StorageException {
try {
return storage.getEncryptionKey(password);
ECKey encryptionFullKey = storage.getEncryptionKey(password);
if(verifyPassword && !ECKey.fromPublicOnly(encryptionFullKey).equals(storage.getEncryptionPubKey())) {
throw new InvalidPasswordException("Derived pubkey does not match stored pubkey");
}
return encryptionFullKey;
} finally {
password.clear();
}
@@ -5,14 +5,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.Map;
public class WalletBackupAndKey {
public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
private final Wallet wallet;
private final Wallet backupWallet;
private final ECKey encryptionKey;
private final Key key;
private final Map<Storage, WalletBackupAndKey> childWallets;
private final Map<WalletBackupAndKey, Storage> childWallets;
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<Storage, WalletBackupAndKey> childWallets) {
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletBackupAndKey, Storage> childWallets) {
this.wallet = wallet;
this.backupWallet = backupWallet;
this.encryptionKey = encryptionKey;
@@ -36,7 +36,7 @@ public class WalletBackupAndKey {
return key;
}
public Map<Storage, WalletBackupAndKey> getChildWallets() {
public Map<WalletBackupAndKey, Storage> getChildWallets() {
return childWallets;
}
@@ -48,4 +48,9 @@ public class WalletBackupAndKey {
key.clear();
}
}
@Override
public int compareTo(WalletBackupAndKey other) {
return wallet.compareTo(other.wallet);
}
}
@@ -45,16 +45,20 @@ public interface BlockTransactionDao {
Map<Sha256Hash, BlockTransaction> existing = getForTxId(txid.getBytes());
if(existing.isEmpty() && blkTx.getId() == null) {
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), truncate(blkTx.getLabel()),
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId());
blkTx.setId(id);
} else {
Long existingId = existing.get(txid) != null ? existing.get(txid).getId() : blkTx.getId();
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), truncate(blkTx.getLabel()),
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId(), existingId);
blkTx.setId(existingId);
}
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
}
}
@@ -88,22 +88,23 @@ public class DbPersistence implements Persistence {
Persistence backupPersistence = PersistenceType.DB.getInstance();
backupPersistence.setKeyDeriver(keyDeriver);
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
backupPersistence.close();
}
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
}
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
List<String> schemas = jdbi.withHandle(handle -> {
return handle.createQuery("show schemas").mapTo(String.class).list();
});
List<String> childSchemas = schemas.stream().filter(schema -> schema.startsWith(WALLET_SCHEMA_PREFIX) && !schema.equals(MASTER_SCHEMA)).collect(Collectors.toList());
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
for(String schema : childSchemas) {
migrate(storage, schema, encryptionKey);
@@ -116,7 +117,7 @@ public class DbPersistence implements Persistence {
return childWallet;
});
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null);
childWallets.put(storage, new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()));
childWallets.put(new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;
@@ -180,10 +181,18 @@ public class DbPersistence implements Persistence {
return;
}
log.debug("Updating " + wallet.getName() + " on " + Thread.currentThread().getName());
log.debug("Updating " + wallet.getFullName() + " on " + Thread.currentThread().getName());
log.debug(dirtyPersistables.toString());
Jdbi jdbi = getJdbi(storage, password);
List<String> schemas = jdbi.withHandle(handle -> {
return handle.createQuery("show schemas").mapTo(String.class).list();
});
if(!schemas.contains(getSchema(wallet))) {
log.debug("Not persisting update for missing schema " + getSchema(wallet));
return;
}
jdbi.useHandle(handle -> {
WalletDao walletDao = handle.attach(WalletDao.class);
try {
@@ -237,8 +246,20 @@ public class DbPersistence implements Persistence {
for(Entry entry : dirtyPersistables.labelEntries) {
if(entry instanceof TransactionEntry && ((TransactionEntry)entry).getBlockTransaction().getId() != null) {
blockTransactionDao.updateLabel(((TransactionEntry)entry).getBlockTransaction().getId(), entry.getLabel());
} else if(entry instanceof NodeEntry && ((NodeEntry)entry).getNode().getId() != null) {
walletNodeDao.updateNodeLabel(((NodeEntry)entry).getNode().getId(), entry.getLabel());
} else if(entry instanceof NodeEntry) {
WalletNode addressNode = ((NodeEntry)entry).getNode();
if(addressNode.getId() == null) {
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
if(purposeNode.getId() == null) {
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
purposeNode.setId(purposeNodeId);
}
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
addressNode.setId(nodeId);
}
walletNodeDao.updateNodeLabel(addressNode.getId(), entry.getLabel());
} else if(entry instanceof HashIndexEntry && ((HashIndexEntry)entry).getHashIndex().getId() != null) {
walletNodeDao.updateTxoLabel(((HashIndexEntry)entry).getHashIndex().getId(), entry.getLabel());
}
@@ -252,6 +273,24 @@ public class DbPersistence implements Persistence {
}
}
if(dirtyPersistables.mixConfig) {
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
}
if(!dirtyPersistables.changedUtxoMixes.isEmpty()) {
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : dirtyPersistables.changedUtxoMixes.entrySet()) {
utxoMixDataDao.addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
}
}
if(!dirtyPersistables.removedUtxoMixes.isEmpty()) {
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
List<Long> ids = dirtyPersistables.removedUtxoMixes.values().stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList());
utxoMixDataDao.deleteUtxoMixData(ids);
}
if(!dirtyPersistables.labelKeystores.isEmpty()) {
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
for(Keystore keystore : dirtyPersistables.labelKeystores) {
@@ -387,6 +426,11 @@ public class DbPersistence implements Persistence {
return null;
}
@Override
public boolean isPersisted(Storage storage, Wallet wallet) {
return wallet.getId() != null;
}
@Override
public ECKey getEncryptionKey(CharSequence password) throws IOException {
return getEncryptionKey(password, null, null);
@@ -555,7 +599,7 @@ public class DbPersistence implements Persistence {
log.error("Wallet file may already be in use. Make sure the application is not running elsewhere.", e);
throw new StorageException("Wallet file may already be in use. Make sure the application is not running elsewhere.", e);
} else if(e.getMessage() != null && (e.getMessage().contains("Wrong user name or password") || e.getMessage().contains("Encryption error in file"))) {
throw new InvalidPasswordException("Incorrect password for wallet file.", e);
throw new InvalidPasswordException("Incorrect password for wallet file " + walletFile.getAbsolutePath(), e);
} else {
log.error("Failed to open database file", e);
throw new StorageException("Failed to open database file.\n" + e.getMessage(), e);
@@ -610,7 +654,22 @@ public class DbPersistence implements Persistence {
@Subscribe
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.add(event.getUtxo());
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos());
}
}
@Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).mixConfig = true;
}
}
@Subscribe
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).changedUtxoMixes.putAll(event.getChangedUtxoMixes());
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).removedUtxoMixes.putAll(event.getRemovedUtxoMixes());
}
}
@@ -634,6 +693,9 @@ public class DbPersistence implements Persistence {
public Integer blockHeight = null;
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean mixConfig;
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
@@ -646,6 +708,9 @@ public class DbPersistence implements Persistence {
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
"\nUTXO statuses:" + utxoStatuses +
"\nMix config:" + mixConfig +
"\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
}
@@ -68,7 +68,7 @@ public interface KeystoreDao {
}
}
long id = insert(keystore.getLabel(), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
keystore.hasPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
keystore.getKeyDerivation().getDerivationPath(),
keystore.hasPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
@@ -99,4 +99,8 @@ public interface KeystoreDao {
}
}
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
}
}
@@ -0,0 +1,41 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
public interface MixConfigDao {
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
@RegisterRowMapper(MixConfigMapper.class)
MixConfig getForWalletId(Long id);
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
default void addMixConfig(Wallet wallet) {
if(wallet.getMixConfig() != null) {
addOrUpdate(wallet, wallet.getMixConfig());
}
}
default void addOrUpdate(Wallet wallet, MixConfig mixConfig) {
String mixToWalletFile = null;
if(mixConfig.getMixToWalletFile() != null) {
mixToWalletFile = mixConfig.getMixToWalletFile().getAbsolutePath();
}
if(mixConfig.getId() == null) {
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
mixConfig.setId(id);
} else {
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
}
}
}
@@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.MixConfig;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
public class MixConfigMapper implements RowMapper<MixConfig> {
@Override
public MixConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
String scode = rs.getString("scode");
Boolean mixOnStartup = rs.getBoolean("mixOnStartup");
if(rs.wasNull()) {
mixOnStartup = null;
}
String mixToWalletFile = rs.getString("mixToWalletFile");
String mixToWalletName = rs.getString("mixToWalletName");
Integer minMixes = rs.getInt("minMixes");
if(rs.wasNull()) {
minMixes = null;
}
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
mixConfig.setId(rs.getLong("id"));
return mixConfig;
}
}
@@ -0,0 +1,56 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.customizer.BindList;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.List;
import java.util.Map;
public interface UtxoMixDataDao {
@SqlQuery("select id, hash, mixesDone, expired from utxoMixData where wallet = ? order by id")
@RegisterRowMapper(UtxoMixDataMapper.class)
Map<Sha256Hash, UtxoMixData> getForWalletId(Long id);
@SqlQuery("select id, hash, mixesDone, expired from utxoMixData where hash = ?")
@RegisterRowMapper(UtxoMixDataMapper.class)
Map<Sha256Hash, UtxoMixData> getForHash(byte[] hash);
@SqlUpdate("insert into utxoMixData (hash, mixesDone, expired, wallet) values (?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertUtxoMixData(byte[] hash, int mixesDone, Long expired, long wallet);
@SqlUpdate("update utxoMixData set hash = ?, mixesDone = ?, expired = ?, wallet = ? where id = ?")
void updateUtxoMixData(byte[] hash, int mixesDone, Long expired, long wallet, long id);
@SqlUpdate("delete from utxoMixData where id in (<ids>)")
void deleteUtxoMixData(@BindList("ids") List<Long> ids);
@SqlUpdate("delete from utxoMixData where wallet = ?")
void clear(long wallet);
default void addUtxoMixData(Wallet wallet) {
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : wallet.getUtxoMixes().entrySet()) {
utxoMixDataEntry.getValue().setId(null);
addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
}
}
default void addOrUpdate(Wallet wallet, Sha256Hash hash, UtxoMixData utxoMixData) {
Map<Sha256Hash, UtxoMixData> existing = getForHash(hash.getBytes());
if(existing.isEmpty() && utxoMixData.getId() == null) {
long id = insertUtxoMixData(hash.getBytes(), utxoMixData.getMixesDone(), utxoMixData.getExpired(), wallet.getId());
utxoMixData.setId(id);
} else {
Long existingId = existing.get(hash) != null ? existing.get(hash).getId() : utxoMixData.getId();
updateUtxoMixData(hash.getBytes(), utxoMixData.getMixesDone(), utxoMixData.getExpired(), wallet.getId(), existingId);
utxoMixData.setId(existingId);
}
}
}
@@ -0,0 +1,42 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class UtxoMixDataMapper implements RowMapper<Map.Entry<Sha256Hash, UtxoMixData>> {
@Override
public Map.Entry<Sha256Hash, UtxoMixData> map(ResultSet rs, StatementContext ctx) throws SQLException {
Sha256Hash hash = Sha256Hash.wrap(rs.getBytes("hash"));
Long expired = rs.getLong("expired");
if(rs.wasNull()) {
expired = null;
}
UtxoMixData utxoMixData = new UtxoMixData(rs.getInt("mixesDone"), expired);
utxoMixData.setId(rs.getLong("id"));
return new Map.Entry<>() {
@Override
public Sha256Hash getKey() {
return hash;
}
@Override
public UtxoMixData getValue() {
return utxoMixData;
}
@Override
public UtxoMixData setValue(UtxoMixData value) {
return null;
}
};
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.UtxoMixData;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import org.jdbi.v3.sqlobject.CreateSqlObject;
@@ -29,6 +30,12 @@ public interface WalletDao {
@CreateSqlObject
BlockTransactionDao createBlockTransactionDao();
@CreateSqlObject
MixConfigDao createMixConfigDao();
@CreateSqlObject
UtxoMixDataDao createUtxoMixDataDao();
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadAllWallets();
@@ -86,6 +93,11 @@ public interface WalletDao {
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
wallet.updateTransactions(blockTransactions);
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
wallet.getUtxoMixes().putAll(utxoMixes);
}
default void addWallet(String schema, Wallet wallet) {
@@ -93,14 +105,20 @@ public interface WalletDao {
setSchema(schema);
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
long id = insert(wallet.getName(), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
long id = insert(truncate(wallet.getName()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
wallet.setId(id);
createKeystoreDao().addKeystores(wallet);
createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet);
createMixConfigDao().addMixConfig(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet);
} finally {
setSchema(DbPersistence.DEFAULT_SCHEMA);
}
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
}
}
@@ -58,10 +58,10 @@ public interface WalletNodeDao {
default void addWalletNodes(Wallet wallet) {
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
purposeNode.setId(purposeNodeId);
for(WalletNode addressNode : purposeNode.getChildren()) {
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNodeId);
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
addressNode.setId(addressNodeId);
addTransactionOutputs(addressNode);
}
@@ -84,22 +84,22 @@ public interface WalletNodeDao {
if(txo.isSpent()) {
BlockTransactionHashIndex spentBy = txo.getSpentBy();
if(spentBy.getId() == null) {
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), truncate(spentBy.getLabel()), spentBy.getIndex(), spentBy.getValue(),
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId());
spentBy.setId(spentById);
} else {
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), truncate(spentBy.getLabel()), spentBy.getIndex(), spentBy.getValue(),
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId(), spentBy.getId());
spentById = spentBy.getId();
}
}
if(txo.getId() == null) {
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), truncate(txo.getLabel()), txo.getIndex(), txo.getValue(),
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId());
txo.setId(txoId);
} else {
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), truncate(txo.getLabel()), txo.getIndex(), txo.getValue(),
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId(), txo.getId());
}
}
@@ -113,4 +113,8 @@ public interface WalletNodeDao {
clearSpentHistory(wallet.getId());
clearHistory(wallet.getId());
}
default String truncate(String label) {
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
}
}
@@ -2,11 +2,13 @@ package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
import com.sparrowwallet.sparrow.io.*;
import javafx.fxml.FXML;
import javafx.scene.control.Accordion;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class HwAirgappedController extends KeystoreImportDetailController {
@@ -16,14 +18,18 @@ public class HwAirgappedController extends KeystoreImportDetailController {
public void initializeView() {
List<KeystoreFileImport> importers = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDIY());
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
}
for(KeystoreImport importer : importers) {
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);;
importAccordion.getPanes().add(importPane);
for(KeystoreFileImport importer : importers) {
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
importAccordion.getPanes().add(importPane);
}
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
}
}
@@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
public void initializeView(List<Device> devices) {
for(Device device : devices) {
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1);
deviceAccordion.getPanes().add(devicePane);
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
deviceAccordion.getPanes().add(devicePane);
}
}
}
}

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