Compare commits

...
159 Commits
Author SHA1 Message Date
Craig Raw 5357b55ef4 v1.6.1 2022-03-04 14:46:33 +02:00
Craig Raw a10bdef484 add paynym addresses dialog 2022-03-04 14:44:22 +02:00
Craig Raw 58f20dab60 update optimization buttons after max button pressed 2022-03-04 11:53:12 +02:00
Craig Raw 1c7abc1b24 fix regression with expanded transaction diagram dark theme background 2022-03-04 11:17:58 +02:00
Craig Raw 02e0fd1357 roll back to preferring ipv4 over ipv6 addresses 2022-03-04 10:38:40 +02:00
Craig Raw 7b3ff2a6d3 fix various refresh history issues when updating node sets 2022-03-04 10:36:47 +02:00
Craig Raw 001e368775 v1.6.0 2022-03-03 14:21:53 +02:00
Craig Raw aebc670b62 fix truncation of alert content in windows 2022-03-03 13:14:12 +02:00
Craig Raw 81810fced5 buffer nodehistorychangedevents to avoid multiple simultaneous history refreshes 2022-03-03 12:45:44 +02:00
Craig Raw 416fc83b4d add protection to recursive refresh call 2022-03-03 09:44:47 +02:00
Craig Raw e594007af1 use system preference to choose between ipv4 and ipv6 addresses from a host which offers both 2022-03-03 08:49:08 +02:00
Craig Raw 3aee0acebb improve logging on incomplete transaction entries 2022-03-03 08:27:22 +02:00
Craig Raw 78a6ce4237 decrease show delay on table tooltips 2022-03-02 17:30:21 +02:00
Craig Raw 414c12aae4 followup #2 2022-03-02 17:15:30 +02:00
Craig Raw 724f9a5211 followup 2022-03-02 17:08:31 +02:00
Craig Raw 1a9c6f8f80 improve bip39 keystore import labelling to indicate generation and import options 2022-03-02 17:01:13 +02:00
Craig Raw b640ffea44 update foundation devices logo 2022-03-02 16:40:06 +02:00
Craig Raw 6cf40e327c prevent wallet response updates while whirlpool is starting 2022-03-02 16:15:27 +02:00
Craig Raw 5959b00611 introduce nested wallet support to allow child wallets to contribute to the master wallet 2022-03-02 13:36:38 +02:00
Craig Raw ce6b371206 refactor paynym functionality to rely on bip47 support 2022-02-23 09:57:56 +02:00
Craig Raw e83c02653c implement bip47 (linking, sending to and receiving from paynyms) 2022-02-22 12:04:39 +02:00
Craig Raw 487be2efb4 fix resolution of mdns (.local) hostnames when connecting to bitcoin core 2022-02-15 14:59:07 +02:00
Craig Raw 91d491f5ec add bip39 wallet import with discovery using common script types and derivations 2022-02-15 13:36:55 +02:00
Craig Raw 9ec57b1ef6 use smaller shared stage icon to reduce memory usage 2022-02-11 12:04:51 +02:00
Craig Raw cc31b5b78e mixing utxos should reflect in send selected total on utxos tab 2022-02-11 09:47:15 +02:00
Craig Raw c056b6240e add gpg key to readme 2022-02-10 19:31:46 +02:00
Craig Raw cb8164c27f add border to undecorated stages on windows 2022-02-10 12:21:36 +02:00
Craig Raw 2ca286d826 remove tmp backup approach for retaining labels over wallet restarts while refreshing, replaced by detached labels 2022-02-09 16:09:12 +02:00
Craig Raw dd7a3a6c8a followup 2022-02-09 13:23:50 +02:00
Craig Raw 7aeca7ebd3 detach and store labels before a wallet refresh, and label matching entries from this store as the wallet is updated 2022-02-09 11:44:38 +02:00
Craig Raw 4e4fd7501c add link to server preferences in status bar on connection failure 2022-02-07 14:08:06 +02:00
Craig Raw a68eeb4669 fix expanding grey area in transaction viewer when increasing hex area height 2022-02-07 12:24:00 +02:00
Craig Raw ebf7a3f177 add seedsigner to wallet import list 2022-02-07 10:28:09 +02:00
Craig Raw 72b15967cc remove whirlpool child wallets from sweep to list 2022-02-03 14:49:09 +02:00
Craig Raw f75b4582c8 v1.5.6 2022-02-03 13:23:37 +02:00
Craig Raw 5d823571df remap partial batch successes to original ids, lock menu functionality when wallet is locked 2022-02-03 12:45:01 +02:00
Craig Raw cca61d281c followup #3 2022-02-02 17:33:21 +02:00
Craig Raw 34c9bc9b69 followup #2 2022-02-02 17:04:38 +02:00
Craig Raw 5fa048d242 followup 2022-02-02 16:34:12 +02:00
Craig Raw ca928fc136 expand transaction diagram in popup on click 2022-02-02 16:03:08 +02:00
Craig Raw 9bf53ab0cd request focus on password field for encrypted locked wallets when wallet window becomes active 2022-02-02 08:58:39 +02:00
Craig Raw 542cc7de6f add heap dump on oom to gradle run args 2022-02-01 10:52:09 +02:00
Craig Raw 77fde3cda9 improve label cell performance by avoiding clipboard retrieval 2022-02-01 10:08:58 +02:00
Craig Raw 1eb595823b search across all wallet accounts, reveal child items if selected 2022-01-31 19:14:50 +02:00
Craig Raw 6d2167428f add wallet search dialog for labels, address, values and txids 2022-01-31 17:15:30 +02:00
Craig Raw 3820b9838d tune batch page size for better performance over tor 2022-01-31 12:11:27 +02:00
Craig Raw 20a99e3236 indicate payment label is required 2022-01-28 15:10:17 +02:00
Craig Raw c81f3d9f5d remove aopp 2022-01-27 22:05:54 +02:00
Craig Raw 526de33bdd (re)allow full addresses in whirlpool child wallet address csv exports 2022-01-26 10:04:45 +01:00
Craig Raw ee992b7255 fix disappearing watch only accounts with the same derivation path 2022-01-25 13:47:03 +01:00
Craig Raw 7d459a9115 detect fulcrum batching version, ensure monotonically increasing ids are used for all requests in a session 2022-01-25 11:50:06 +01:00
Craig Raw 9faf036e4d improve wallet loading performance 2022-01-20 17:12:38 +02:00
Craig Raw 306f241a4a make mix config button naming clearer 2022-01-20 08:27:48 +02:00
Craig Raw 41d1a1806d improve deep wallet load performance by adding a setting to watch only the last x used addresses 2022-01-19 13:50:03 +02:00
Craig Raw a825a693c1 fix loss of transaction labels when switching servers 2022-01-19 09:41:53 +02:00
Craig Raw 7423d94935 only retain one day of mempool rate sizes 2022-01-14 08:26:59 +02:00
Craig Raw 82f9a0f2af add fee rate and rbf information for unconfirmed transactions in transactions tab 2022-01-13 17:34:31 +02:00
Craig Raw 7da62bb135 increase max utxo chart bars 2022-01-13 16:37:16 +02:00
Craig Raw f2e5259916 fix npe when performing soroban reply without utxos 2022-01-13 16:22:20 +02:00
Craig Raw 4f4a48eb98 fix mix out probability percentage in tooltip 2022-01-13 16:05:29 +02:00
Craig Raw 9c3b647f07 add tool to sweep a private key in wif format to any address 2022-01-12 15:44:13 +02:00
Craig Raw 7f2d72ee59 pass psbt and message to hwi on stdin to avoid too long process arguments 2022-01-10 12:01:03 +02:00
Craig Raw 548b8d270b upgrade nightjar to avoid shutdown of httpclient when disconnecting from whirlpool websocket 2022-01-10 09:08:37 +02:00
Craig Raw 696746f06c update nightjar to avoid using jetty shutdownthread 2022-01-09 13:09:54 +02:00
Craig Raw 6f11a20feb do db updates in background thread and improve efficiency when refreshing a deep wallet 2022-01-09 11:06:17 +02:00
Craig Raw c28c2ed506 upgrade to h2 2.0.206 2022-01-07 09:52:54 +02:00
Craig Raw 8fb7f544de add broadcasting step to soroban initiator dialog and indicate when transaction has been successfully broadcasted 2022-01-07 09:46:10 +02:00
Craig Raw a76d9dba21 indicate output descriptor key expressions are shown in canonical order 2022-01-05 13:14:41 +02:00
Craig Raw f1b3f7d5dd fix regtest public server preference regression 2022-01-05 12:41:14 +02:00
Craig Raw fd9e19d052 dont enable max button on clear 2022-01-04 11:08:32 +02:00
Craig Raw 56363c083e adapt to non-commented derivation entries for multiple derivation path wallets in coldcard export file (> 3.2.1) 2022-01-04 10:57:22 +02:00
Craig Raw bbdfec127a rename gordian seed tool, support retrieving a keystore from a scanned crypto-output 2022-01-04 10:34:32 +02:00
Craig Raw eb1087bf8d upgrade to h2 2.0.204 2022-01-04 09:30:46 +02:00
Craig Raw 796f68640c use unique (per session) integers as ids for all paged server queries 2021-12-22 11:16:55 +02:00
Craig Raw ad091217d6 set bip47 wallet from an associated wallet 2021-12-16 13:05:39 +02:00
Craig Raw 40e06b96a9 only follow paynym if bip47 wallet is loaded 2021-12-16 12:54:11 +02:00
Craig Raw 3fd186e22c v1.5.5 2021-12-16 11:00:54 +02:00
Craig Raw e8c7f57704 update seedsigner import description for 0.4.5 firmware 2021-12-16 10:58:21 +02:00
Craig Raw e6de33034b add seed tool as an airgapped hardware wallet 2021-12-16 10:06:35 +02:00
Craig Raw 73b6b9219b handle offline state when initiating a collaborative mix 2021-12-16 08:56:01 +02:00
Craig Raw d1d1b0806f fix about dialog text 2021-12-16 08:35:36 +02:00
Craig Raw 5ee97966ee upgrade to logback 1.2.8 2021-12-16 08:28:14 +02:00
Craig Raw 4819f329ae dont allow soroban mix to start if initiator is not connected 2021-12-15 16:45:40 +02:00
Craig Raw 52aed8a3f5 update nightjar to permit non-zero counterparty accounts in stowaways 2021-12-15 16:08:15 +02:00
Craig Raw 86ef129c1b follow up for theming 2021-12-15 14:28:31 +02:00
Craig Raw aff872eea0 show relative sizes of amounts in transaction diagram 2021-12-15 14:14:08 +02:00
Craig Raw 5d0025b4a7 allow positive amounts below dust limit when sending to paynyms 2021-12-15 09:42:30 +02:00
Craig Raw e625a4e542 remove account using tab context menu 2021-12-15 09:11:38 +02:00
Craig Raw 72ff1df61e add menu item to show paynym 2021-12-14 20:08:56 +02:00
Craig Raw 3776fbafd9 set txo label to payment label when sending multiple consolidation outputs 2021-12-14 18:53:48 +02:00
Craig Raw cd91aff3bb show entered labels in transaction view diagram when sending to multiple recipients 2021-12-14 18:24:37 +02:00
Craig Raw b530ced9ed support scanning crypto-account and crypto-output through both qr scans on settings tab 2021-12-14 11:21:32 +02:00
Craig Raw 22957e9d88 fix tx input issue 2021-12-13 16:23:46 +02:00
Craig Raw d76aecb34d handle quotes in connected device passphrase on windows 2021-12-13 14:27:35 +02:00
Craig Raw 4cbb402931 fix mempool size chart tooltip legend 2021-12-13 13:35:22 +02:00
Craig Raw 4da82b110c various minor fixes 2021-12-13 12:45:21 +02:00
Craig Raw 880096a193 v1.5.4 2021-12-10 08:07:20 +02:00
Craig Raw 06c0fb8594 add menu item to lock all open wallets in a window 2021-12-10 07:59:44 +02:00
Craig Raw 8e85543c6e fix module related build issue prevent qr scanner from opening 2021-12-10 07:57:56 +02:00
Craig Raw 4fb72fdf89 v1.5.3 2021-12-09 13:07:04 +02:00
Craig Raw cf2616ec2b show progress indicator when retrieving paynym 2021-12-09 12:04:53 +02:00
Craig Raw 238aae5ea9 follow up 2021-12-09 11:51:24 +02:00
Craig Raw a963d10381 change windows and linux installers to use sparrow menu group 2021-12-09 11:50:32 +02:00
Craig Raw eb90d6a31a constrain locktime datetimepicker to show valid values only 2021-12-09 10:28:33 +02:00
Craig Raw b013b5f50f add paging for batched server requests with configurable page size defaulting to 500 ids 2021-12-09 09:29:50 +02:00
Craig Raw 14db333a6f improve passphrase toggle of bitbox02 and trezor t 2021-12-08 14:39:16 +02:00
Craig Raw 6804f713b2 update hummingbird to v1.6.4 (revised crypto-account format) 2021-12-08 10:56:33 +02:00
Craig Raw 90a2c3b89b improve ux when validating transaction locktime datetime field 2021-12-08 10:20:21 +02:00
Craig Raw 3c94664ac3 update utxo tab ui to show utxo balance and count next to smaller utxo chart 2021-12-07 13:29:03 +02:00
Craig Raw 1b61a78e6d add minimum fee rates source of static 1 sat/vb for all block targets 2021-12-07 10:26:33 +02:00
Craig Raw 1defe51fd7 add button to view password field contents 2021-12-06 12:06:51 +02:00
Craig Raw 4cbd778ca1 show wallet display name on mix to button 2021-12-03 14:02:48 +02:00
Craig Raw e59ee47624 remember and select previously selected tab when closing a tab 2021-12-03 12:21:09 +02:00
Craig Raw 8b42399423 allow configuration of a maximum server timeout (maxServerTimeout) in sparrow config 2021-12-03 11:16:01 +02:00
Craig Raw e84f82f47b follow up 2021-12-03 10:45:23 +02:00
Craig Raw e4189711bd improve server connection and wallet loading pulse animation efficiency 2021-12-03 10:34:59 +02:00
Craig Raw 9bca911b0b show only unspent amount in status bar when refreshing postmix wallets 2021-12-02 14:24:09 +02:00
Craig Raw eb498f2bcc improve error handling for paynym.is connection issues 2021-12-02 13:33:30 +02:00
Craig Raw 3d13f596a0 v1.5.3-beta1 2021-12-01 16:07:33 +02:00
Craig Raw 28984427e1 upgrade to h2 2.0.202 2021-12-01 15:37:28 +02:00
Craig Raw ed92acc468 change default min mix to = 3, various minor ux improvements 2021-12-01 15:10:31 +02:00
Craig Raw 3b9c87abc7 improve incorrect password handling on final mix confirm 2021-12-01 14:43:48 +02:00
Craig Raw 00181875c1 followup 2021-12-01 14:14:13 +02:00
Craig Raw 26fb2b97fb add pay to paynym via payjoin 2021-12-01 14:11:16 +02:00
Craig Raw 44194a074c add and integrate paynym dialog 2021-11-30 14:43:23 +02:00
Craig Raw 4edd84f6e2 collaborative mix aesthetic tweaks 2021-11-29 17:08:58 +02:00
Craig Raw 0956c96046 integrate paynyms to collaborative mixing 2021-11-29 15:31:33 +02:00
Craig Raw 3013688447 trigger full wallet refresh when all transaction history has changed on loading 2021-11-26 14:05:56 +02:00
Craig Raw 0302913c3f create two person coinjoin transactions using soroban 2021-11-25 16:15:59 +02:00
Craig Raw 72768362a5 avoid concurrent modification error 2021-11-23 09:21:50 +02:00
Craig Raw 39fa65ea37 restart whirlpool if no utxos mixing, bind debug logging accelerator 2021-11-23 09:17:19 +02:00
Craig Raw 4554c9d0df catch and show hwi enumerate errors 2021-11-17 17:42:50 +02:00
Craig Raw 9b9b295045 force save of temp backup if refreshed wallet transactions are less 2021-11-17 17:13:41 +02:00
Craig Raw ece786131e check if wallet is mixing and restart whirlpool client if necessary 2021-11-17 16:02:11 +02:00
Craig Raw dc65313313 show wallet output descriptors with multipath indexes as per bitcoin core pr #22838 2021-11-13 15:05:45 +02:00
Craig Raw cb41a1ed66 fix import of encrypted json wallet on linux 2021-11-13 10:05:05 +02:00
Craig Raw c566dea232 support tr script expressions in qr crypto-output 2021-11-12 17:18:24 +02:00
Craig Raw 8f04d23b3f add utxo set replacement ui 2021-11-12 16:46:05 +02:00
Craig Raw b8b1039ada show utxo sets in transaction diagram 2021-11-12 15:54:49 +02:00
Craig Raw a7aafa27d0 indicate which accounts are scanned in info dialog 2021-11-12 10:07:53 +02:00
Craig Raw 18a1e82dda fix linux mix to label ellipsis 2021-11-11 18:08:56 +02:00
Craig Raw e26587e807 fix combobox texfield repeat selection 2021-11-11 15:32:37 +02:00
Craig Raw c3778b6419 fix version update hyperlink appearing multiple times 2021-11-10 16:13:23 +02:00
Craig Raw fb85277894 output hwi signing exception to log 2021-11-09 12:35:06 +02:00
Craig Raw 8ae3399d56 fix to avoid scanning with guide box 2021-11-08 11:49:17 +02:00
Craig Raw 7272de90f0 fix mix to wallet display name 2021-11-08 11:11:15 +02:00
Craig Raw 98c1f05ed6 double pass qr with wide and cropped image 2021-11-08 10:30:56 +02:00
Craig Raw 90439501ad improve qr scanning performance by cropping to indicated box 2021-11-07 13:04:20 +02:00
Craig Raw 1bb3833cbe reduce send tab chart min heights 2021-11-05 16:20:35 +02:00
Craig Raw d7d8140050 follow up 2021-11-05 16:01:23 +02:00
Craig Raw 3c77552211 show script type description when importing wallet keystore 2021-11-05 15:57:08 +02:00
Craig Raw 5e7d1d1f69 allow for minimum application height of 708px 2021-11-05 12:59:06 +02:00
Craig Raw 8f92f9ec38 upgrade to javafx 17 2021-11-04 15:19:43 +02:00
Craig Raw af23f063f2 show info dialog when no new accounts are discovered 2021-11-04 14:44:38 +02:00
Craig Raw 13a3ce23e7 make transaction diagram tooltips show indefinitely 2021-11-04 13:21:40 +02:00
Craig Raw d7ff73243c remember recent servers for quick reconfiguration 2021-11-04 11:04:48 +02:00
Craig Raw 25dd0440f6 add show transaction/psbt as qr from file menu 2021-11-03 14:09:49 +02:00
Craig Raw b5301c4556 add public testnet electrum server 2021-11-03 12:27:45 +02:00
Craig Raw a22f69e2c1 improve long fade out animation efficiency 2021-11-02 18:52:37 +02:00
223 changed files with 9950 additions and 1982 deletions
+7 -1
View File
@@ -2,7 +2,7 @@
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [GitHub](https://github.com/sparrowwallet/sparrow/releases).
![Sparrow Wallet](https://sparrowwallet.com/assets/images/control-your-sends.png)
@@ -91,6 +91,12 @@ Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab abo
Sparrow is licensed under the Apache 2 software licence.
## GPG Key
The Sparrow release binaries here and on [sparrowwallet.com](https://sparrowwallet.com/download/) are signed using [craigraw's GPG key](https://keybase.io/craigraw):
Fingerprint: D4D0D3202FC06849A257B38DE94618334C674B40
64-bit: E946 1833 4C67 4B40
## Credit
![Yourkit](https://www.yourkit.com/images/yklogo.png)
+41 -16
View File
@@ -1,11 +1,11 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'extra-java-module-info'
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.5.2'
def sparrowVersion = '1.6.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 = "16"
version = "17"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -45,13 +45,13 @@ dependencies {
}
implementation('com.google.guava:guava:28.2-jre')
implementation('com.google.code.gson:gson:2.8.6')
implementation('com.h2database:h2:1.4.201-SNAPSHOT')
implementation('com.h2database:h2:2.0.206')
implementation('com.zaxxer:HikariCP:4.0.3')
implementation('org.jdbi:jdbi3-core:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') {
@@ -65,7 +65,8 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.2')
implementation('com.sparrowwallet:hummingbird:1.6.4')
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
@@ -91,7 +92,11 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.19')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.32')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
testImplementation('junit:junit:4.12')
}
@@ -122,7 +127,8 @@ test {
}
run {
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
@@ -191,7 +197,8 @@ jlink {
"--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"]
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
@@ -204,14 +211,14 @@ jlink {
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
@@ -354,7 +361,7 @@ extraJavaModuleInfo {
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') {
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
@@ -431,6 +438,11 @@ extraJavaModuleInfo {
requires('javafx.graphics')
requires('javafx.controls')
}
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
exports('io.reactivex.rxjavafx.schedulers')
requires('io.reactivex.rxjava2')
requires('javafx.graphics')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
requires('javafx.base')
requires('javafx.graphics')
@@ -441,7 +453,7 @@ extraJavaModuleInfo {
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') {
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
@@ -449,7 +461,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.19.jar', 'com.sparrowwallet.nightjar', '0.2.19') {
module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
@@ -459,6 +471,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('org.json')
requires('io.reactivex.rxjava2')
exports('com.samourai.http.client')
exports('com.samourai.tor.client')
exports('com.samourai.wallet.api.backend')
@@ -466,6 +479,17 @@ extraJavaModuleInfo {
exports('com.samourai.wallet.client.indexHandler')
exports('com.samourai.wallet.hd')
exports('com.samourai.wallet.util')
exports('com.samourai.wallet.bip47.rpc')
exports('com.samourai.wallet.bip47.rpc.java')
exports('com.samourai.wallet.cahoots')
exports('com.samourai.wallet.cahoots.psbt')
exports('com.samourai.wallet.cahoots.stonewallx2')
exports('com.samourai.soroban.cahoots')
exports('com.samourai.soroban.client')
exports('com.samourai.soroban.client.cahoots')
exports('com.samourai.soroban.client.meeting')
exports('com.samourai.soroban.client.rpc')
exports('com.samourai.wallet.send')
exports('com.samourai.whirlpool.client.event')
exports('com.samourai.whirlpool.client.wallet')
exports('com.samourai.whirlpool.client.wallet.beans')
@@ -483,6 +507,7 @@ extraJavaModuleInfo {
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.chain')
exports('com.samourai.whirlpool.client.wallet.data.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState')
@@ -549,7 +574,7 @@ extraJavaModuleInfo {
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') {
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
@@ -59,7 +59,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if (isModule(originalJar) && !originalJarName.contains("jackson")) {
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
+1 -1
Submodule drongo updated: 6c03c4106a...04631be8c1
-2
View File
@@ -1,2 +0,0 @@
mime-type=x-scheme-handler/aopp
description=Verify Address Ownership URI
@@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Unknown
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin
+1 -9
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.5.2</string>
<string>1.6.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -45,14 +45,6 @@
<string>bitcoin</string>
</array>
</dict>
<dict>
<key>CFBundleURLName</key>
<string>com.sparrowwallet.sparrow.aopp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>aopp</string>
</array>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
-10
View File
@@ -77,16 +77,6 @@
<DirectoryRef Id="TARGETDIR">
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
@@ -17,6 +17,8 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -25,9 +27,14 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import com.sparrowwallet.sparrow.soroban.Soroban;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
@@ -48,17 +55,14 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.controlsfx.control.Notifications;
import org.controlsfx.control.StatusBar;
@@ -90,12 +94,18 @@ public class AppController implements Initializable {
@FXML
private MenuItem saveTransaction;
@FXML
private MenuItem showTransaction;
@FXML
private Menu savePSBT;
@FXML
private MenuItem savePSBTBinary;
@FXML
private MenuItem showPSBT;
@FXML
private MenuItem exportWallet;
@@ -136,10 +146,6 @@ public class AppController implements Initializable {
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();
@@ -150,12 +156,24 @@ public class AppController implements Initializable {
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem searchWallet;
@FXML
private MenuItem refreshWallet;
@FXML
private MenuItem sendToMany;
@FXML
private MenuItem sweepPrivateKey;
@FXML
private MenuItem findMixingPartner;
@FXML
private MenuItem showPayNym;
@FXML
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@@ -176,6 +194,8 @@ public class AppController implements Initializable {
private Timeline statusTimeline;
private Tab previouslySelectedTab;
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
@@ -220,7 +240,10 @@ public class AppController implements Initializable {
rootStack.getStyleClass().removeAll(DRAG_OVER_CLASS);
});
tabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
tabs.getSelectionModel().selectedItemProperty().addListener((observable, previouslySelectedTab, selectedTab) -> {
if(tabs.getTabs().contains(previouslySelectedTab)) {
this.previouslySelectedTab = previouslySelectedTab;
}
tabs.getTabs().forEach(tab -> ((Label)tab.getGraphic()).getGraphic().setOpacity(TAB_LABEL_GRAPHIC_OPACITY_INACTIVE));
if(selectedTab != null) {
Label tabLabel = (Label)selectedTab.getGraphic();
@@ -239,6 +262,10 @@ public class AppController implements Initializable {
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
if(c.wasRemoved() && previouslySelectedTab != null) {
tabs.getSelectionModel().select(previouslySelectedTab);
}
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
if(walletAdded || walletRemoved) {
@@ -300,18 +327,27 @@ public class AppController implements Initializable {
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);
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
showTransaction.disableProperty().bind(saveTransaction.disableProperty());
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true);
lockWallet.setDisable(true);
searchWallet.disableProperty().bind(exportWallet.disableProperty());
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
showPayNym.setDisable(true);
findMixingPartner.setDisable(true);
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
showPayNym.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !getSelectedWalletForm().getWallet().hasPaymentCode() || !newValue);
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
});
setServerType(Config.get().getServerType());
serverToggle.setSelected(isConnected());
@@ -384,12 +420,17 @@ public class AppController implements Initializable {
private Stage getAboutStage() {
try {
FXMLLoader loader = new FXMLLoader(AppController.class.getResource("about.fxml"));
Parent root = loader.load();
StackPane root = loader.load();
AboutController controller = loader.getController();
Stage stage = new Stage();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}
Stage stage = new Stage(StageStyle.UNDECORATED);
stage.setTitle("About " + MainApp.APP_NAME);
stage.initStyle(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? StageStyle.UNDECORATED : StageStyle.DECORATED);
stage.initOwner(tabs.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.setResizable(false);
Scene scene = new Scene(root);
AppServices.onEscapePressed(scene, stage::close);
@@ -588,6 +629,23 @@ public class AppController implements Initializable {
}
}
public void showTransaction(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData) tabData;
Transaction transaction = transactionTabData.getTransaction();
try {
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
qrDisplayDialog.showAndWait();
} catch(Exception e) {
log.error("Error creating UR", e);
}
}
}
public void savePSBTBinary(ActionEvent event) {
savePSBT(false, true);
}
@@ -671,6 +729,18 @@ public class AppController implements Initializable {
}
}
public void showPSBT(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
qrDisplayDialog.show();
}
}
public List<WalletTabData> getOpenWalletTabData() {
List<WalletTabData> openWalletTabData = new ArrayList<>();
@@ -746,12 +816,6 @@ public class AppController implements Initializable {
EventManager.get().post(new LoadingLogChangedEvent(item.isSelected()));
}
public void showUtxosChart(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowUtxosChart(item.isSelected());
EventManager.get().post(new UtxosChartChangedEvent(item.isSelected()));
}
public void showTxHex(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowTransactionHex(item.isSelected());
@@ -802,7 +866,7 @@ public class AppController implements Initializable {
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
Storage storage = new Storage(walletFile);
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
}
}
@@ -827,8 +891,21 @@ public class AppController implements Initializable {
try {
Storage storage = new Storage(file);
if(!storage.isEncrypted()) {
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
loadWalletService.setOnSucceeded(workerStateEvent -> {
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
});
loadWalletService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
if(exception instanceof StorageException) {
showErrorDialog("Error Opening Wallet", exception.getMessage());
} else if(!attemptImportWallet(file, null)) {
log.error("Error opening wallet", exception);
showErrorDialog("Error Opening Wallet", exception.getMessage() == null || exception.getMessage().contains("Expected BEGIN_OBJECT") ? "Unsupported wallet file format." : exception.getMessage());
}
});
loadWalletService.start();
} else {
WalletPasswordDialog dlg = new WalletPasswordDialog(storage.getWalletName(null), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> optionalPassword = dlg.showAndWait();
@@ -840,8 +917,8 @@ public class AppController implements Initializable {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
loadWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Done"));
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
});
loadWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed"));
@@ -852,9 +929,11 @@ public class AppController implements Initializable {
Platform.runLater(() -> openWalletFile(file, forceSameWindow));
}
} else {
if(!attemptImportWallet(file, password)) {
if(exception instanceof StorageException) {
showErrorDialog("Error Opening Wallet", exception.getMessage());
} else if(!attemptImportWallet(file, password)) {
log.error("Error Opening Wallet", exception);
showErrorDialog("Error Opening Wallet", exception.getMessage() == null ? "Unsupported file format" : exception.getMessage());
showErrorDialog("Error Opening Wallet", exception.getMessage() == null || exception.getMessage().contains("Expected BEGIN_OBJECT") ? "Unsupported wallet file format." : exception.getMessage());
}
password.clear();
}
@@ -862,8 +941,6 @@ public class AppController implements Initializable {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.START, "Decrypting wallet..."));
loadWalletService.start();
}
} catch(StorageException e) {
showErrorDialog("Error Opening Wallet", e.getMessage());
} catch(Exception e) {
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
log.error("Error opening wallet", e);
@@ -875,23 +952,23 @@ public class AppController implements Initializable {
}
}
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
try {
checkWalletNetwork(walletBackupAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
if(!walletBackupAndKey.getWallet().isValid()) {
checkWalletNetwork(walletAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
if(!walletAndKey.getWallet().isValid()) {
throw new IllegalStateException("Wallet file is not valid.");
}
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletAndKey.getWallet(), forceSameWindow);
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.getChildWallets().entrySet()) {
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
}
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
Platform.runLater(() -> selectTab(walletAndKey.getWallet()));
} catch(Exception e) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", e.getMessage());
} finally {
walletBackupAndKey.clear();
walletAndKey.clear();
}
}
@@ -902,7 +979,7 @@ public class AppController implements Initializable {
}
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
if(wallet.containsPrivateKeys()) {
if(wallet.containsMasterPrivateKeys()) {
//Derive xpub and master fingerprint from seed, potentially with passphrase
Wallet copy = wallet.copy();
for(int i = 0; i < copy.getKeystores().size(); i++) {
@@ -939,6 +1016,8 @@ public class AppController implements Initializable {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
soroban.setHDWallet(copy);
}
StandardAccount standardAccount = wallet.getStandardAccountType();
@@ -954,16 +1033,29 @@ public class AppController implements Initializable {
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
copyKeystore.getSeed().clear();
} else if(keystore.hasMasterPrivateExtendedKey()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
copyKeystore.getMasterPrivateKey().clear();
}
}
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isBip47()) {
try {
Keystore keystore = childWallet.getKeystores().get(0);
keystore.setBip47ExtendedPrivateKey(wallet.getKeystores().get(0).getBip47ExtendedPrivateKey());
} catch(Exception e) {
log.error("Cannot prepare BIP47 keystore", e);
}
}
}
}
public void importWallet(ActionEvent event) {
@@ -1063,13 +1155,13 @@ public class AppController implements Initializable {
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, null);
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, null);
addWalletTabOrWindow(storage, childWallet, null, false);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
@@ -1090,14 +1182,16 @@ public class AppController implements Initializable {
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, key);
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
childWallet.encrypt(key);
if(!childWallet.isNested()) {
childWallet.encrypt(key);
}
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key);
addWalletTabOrWindow(storage, childWallet, null, false);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
@@ -1135,6 +1229,11 @@ public class AppController implements Initializable {
preferencesDialog.showAndWait();
}
public void openServerPreferences(ActionEvent event) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
preferencesDialog.showAndWait();
}
public void signVerifyMessage(ActionEvent event) {
MessageSignDialog messageSignDialog = null;
WalletForm selectedWalletForm = getSelectedWalletForm();
@@ -1175,6 +1274,90 @@ public class AppController implements Initializable {
}
}
public void sweepPrivateKey(ActionEvent event) {
Wallet wallet = null;
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getWallet().isValid()) {
wallet = selectedWalletForm.getWallet();
}
PrivateKeySweepDialog dialog = new PrivateKeySweepDialog(wallet);
Optional<Transaction> optTransaction = dialog.showAndWait();
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
}
public void findMixingPartner(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
if(soroban.getHdWallet() == null) {
if(wallet.isEncrypted()) {
Wallet copy = wallet.copy();
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage storage = selectedWalletForm.getStorage();
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
copy.decrypt(key);
try {
soroban.setHDWallet(copy);
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE);
}
counterpartyDialog.showAndWait();
} finally {
key.clear();
encryptionFullKey.clear();
password.get().clear();
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> findMixingPartner(null));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
soroban.setHDWallet(wallet);
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE);
}
counterpartyDialog.showAndWait();
}
} else {
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
if(Config.get().isSameAppMixing()) {
counterpartyDialog.initModality(Modality.NONE);
}
counterpartyDialog.showAndWait();
}
}
}
public void showPayNym(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId());
payNymDialog.showAndWait();
}
}
public void minimizeToTray(ActionEvent event) {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
@@ -1186,19 +1369,49 @@ public class AppController implements Initializable {
}
}
public void lockWallets(ActionEvent event) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(!walletTabData.getWalletForm().isLocked()) {
EventManager.get().post(new WalletLockEvent(walletTabData.getWalletForm().getMasterWallet()));
}
}
}
}
public void searchWallet(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
if(selectedTab != null) {
TabData tabData = (TabData) selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane) selectedTab.getContent();
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
if(!walletForms.isEmpty()) {
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
if(optEntry.isPresent()) {
Entry entry = optEntry.get();
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
}
}
}
}
}
public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
Wallet pastWallet = wallet.copy();
selectedWalletForm.getStorage().backupTempWallet();
wallet.clearHistory();
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, selectedWalletForm.getWalletId()));
}
}
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) {
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage.getWalletId(wallet));
if(existingWalletWindow instanceof Stage) {
Stage existingWalletStage = (Stage)existingWalletWindow;
@@ -1213,15 +1426,15 @@ public class AppController implements Initializable {
AppController appController = AppServices.newAppWindow(stage);
stage.toFront();
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
appController.addWalletTab(storage, wallet, backupWallet);
appController.addWalletTab(storage, wallet);
return appController;
} else {
addWalletTab(storage, wallet, backupWallet);
addWalletTab(storage, wallet);
return this;
}
}
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
public void addWalletTab(Storage storage, Wallet wallet) {
if(wallet.isMasterWallet()) {
String name = storage.getWalletName(wallet);
if(!name.equals(wallet.getName())) {
@@ -1251,7 +1464,7 @@ public class AppController implements Initializable {
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
@@ -1279,14 +1492,20 @@ public class AppController implements Initializable {
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(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
Platform.runLater(() -> {
setSubTabsVisible(subTabs, true);
});
if(wallet.isNested()) {
WalletForm walletForm = new WalletForm(storage, wallet);
EventManager.get().register(walletForm);
walletTabData.getWalletForm().getNestedWalletForms().add(walletForm);
} else {
TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet);
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(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
Platform.runLater(() -> {
setSubTabsVisible(subTabs, true);
});
}
}
}
}
@@ -1309,7 +1528,7 @@ public class AppController implements Initializable {
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet) {
try {
Tab subTab = new Tab();
subTab.setClosable(false);
@@ -1327,14 +1546,14 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
//Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance.
WalletForm walletForm = new WalletForm(storage, wallet, backupWallet);
WalletForm walletForm = new WalletForm(storage, wallet);
EventManager.get().register(walletForm);
controller.setWalletForm(walletForm);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
subTab.setUserData(tabData);
if(!wallet.isWhirlpoolChildWallet()) {
subTab.setContextMenu(getSubTabContextMenu(subTab));
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
}
subTabs.getTabs().add(subTab);
@@ -1370,12 +1589,14 @@ public class AppController implements Initializable {
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();
if(selectedTab != null) {
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;
@@ -1562,7 +1783,7 @@ public class AppController implements Initializable {
return contextMenu;
}
private ContextMenu getSubTabContextMenu(Tab subTab) {
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
rename.setOnAction(event -> {
@@ -1573,14 +1794,27 @@ public class AppController implements Initializable {
String label = optLabel.get();
subTabLabel.setText(label);
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
Wallet wallet = walletTabData.getWallet();
wallet.setLabel(label);
EventManager.get().post(new WalletLabelChangedEvent(wallet));
}
});
contextMenu.getItems().add(rename);
if(!wallet.isMasterWallet() && !wallet.isWhirlpoolChildWallet()) {
MenuItem delete = new MenuItem("Delete Account");
delete.setOnAction(event -> {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
subTabs.getTabs().remove(subTab);
if(subTabs.getTabs().size() == 1) {
setSubTabsVisible(subTabs, false);
}
EventManager.get().post(new WalletDeletedEvent(wallet));
}
});
contextMenu.getItems().add(delete);
}
return contextMenu;
}
@@ -1612,21 +1846,17 @@ public class AppController implements Initializable {
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(600), thumbArea);
fadeTransition.setFromValue(1.0);
fadeTransition.setToValue(0.4);
fadeTransition.setAutoReverse(true);
fadeTransition.setCycleCount(Animation.INDEFINITE);
fadeTransition.play();
serverToggle.setUserData(fadeTransition);
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.4, 8);
timeline.play();
serverToggle.setUserData(new AnimationUtil.AnimatedNode(thumbArea, timeline));
}
}
private void serverToggleStopAnimation() {
if(serverToggle.getUserData() != null) {
FadeTransition fadeTransition = (FadeTransition)serverToggle.getUserData();
fadeTransition.stop();
fadeTransition.getNode().setOpacity(1.0);
AnimationUtil.AnimatedNode animatedNode = (AnimationUtil.AnimatedNode)serverToggle.getUserData();
animatedNode.timeline().stop();
animatedNode.node().setOpacity(1.0);
serverToggle.setUserData(null);
}
}
@@ -1642,13 +1872,9 @@ public class AppController implements Initializable {
private void tabLabelStartAnimation(Tab tab) {
Label tabLabel = (Label) tab.getGraphic();
if(tabLabel.getUserData() == null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), tabLabel.getGraphic());
fadeTransition.setFromValue(tabLabel.getGraphic().getOpacity());
fadeTransition.setToValue(0.1);
fadeTransition.setAutoReverse(true);
fadeTransition.setCycleCount(Animation.INDEFINITE);
fadeTransition.play();
tabLabel.setUserData(fadeTransition);
Timeline timeline = AnimationUtil.getPulse(tabLabel.getGraphic(), Duration.millis(1000), tabLabel.getGraphic().getOpacity(), 0.1, 8);
timeline.play();
tabLabel.setUserData(timeline);
}
}
@@ -1678,8 +1904,8 @@ public class AppController implements Initializable {
private void tabLabelStopAnimation(Tab tab) {
Label tabLabel = (Label) tab.getGraphic();
if(tabLabel.getUserData() != null) {
FadeTransition fadeTransition = (FadeTransition)tabLabel.getUserData();
fadeTransition.stop();
Animation animation = (Animation)tabLabel.getUserData();
animation.stop();
tabLabel.setUserData(null);
tabLabel.getGraphic().setOpacity(tab.isSelected() ? TAB_LABEL_GRAPHIC_OPACITY_ACTIVE : TAB_LABEL_GRAPHIC_OPACITY_INACTIVE);
}
@@ -1751,18 +1977,20 @@ public class AppController implements Initializable {
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
showTxHex.setDisable(false);
showPayNym.setDisable(true);
findMixingPartner.setDisable(true);
} else if(event instanceof WalletTabSelectedEvent) {
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
showTxHex.setDisable(true);
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
}
}
}
@@ -1786,7 +2014,9 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
exportWallet.setDisable(!event.getWallet().isValid());
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode() || !AppServices.onlineProperty().get());
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
}
}
}
@@ -1809,7 +2039,7 @@ public class AppController implements Initializable {
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
List<BlockTransaction> whirlpoolTransactions = event.getUnspentConfirmingWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
if(!whirlpoolTransactions.isEmpty()) {
@@ -1865,12 +2095,9 @@ public class AppController implements Initializable {
});
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();
String walletName = event.getWallet().getFullDisplayName();
if(walletName.length() > 40) {
walletName = walletName.substring(0, 40) + "...";
}
Notifications notificationBuilder = Notifications.create()
@@ -1895,6 +2122,7 @@ public class AppController implements Initializable {
@Subscribe
public void statusUpdated(StatusEvent event) {
statusBar.setText(event.getStatus());
statusBar.setGraphic(event.getGraphic());
if(wait != null && wait.getStatus() == Animation.Status.RUNNING) {
wait.stop();
@@ -1903,6 +2131,7 @@ public class AppController implements Initializable {
wait.setOnFinished((e) -> {
if(statusBar.getText().equals(event.getStatus())) {
statusBar.setText("");
statusBar.setGraphic(null);
}
});
wait.play();
@@ -1916,8 +2145,15 @@ public class AppController implements Initializable {
AppServices.get().getApplication().getHostServices().showDocument("https://www.sparrowwallet.com/download");
});
if(statusBar.getRightItems().size() > 0 && statusBar.getRightItems().get(0) instanceof Hyperlink) {
statusBar.getRightItems().remove(0);
Hyperlink existingUpdateLabel = null;
for(Node node : statusBar.getRightItems()) {
if(node instanceof Hyperlink) {
existingUpdateLabel = (Hyperlink)node;
}
}
if(existingUpdateLabel != null) {
statusBar.getRightItems().remove(existingUpdateLabel);
}
statusBar.getRightItems().add(0, versionUpdateLabel);
@@ -1925,6 +2161,7 @@ public class AppController implements Initializable {
@Subscribe
public void timedWorker(TimedEvent event) {
statusBar.setGraphic(null);
if(event.getTimeMills() == 0) {
if(statusTimeline != null && statusTimeline.getStatus() == Animation.Status.RUNNING) {
statusTimeline.stop();
@@ -1940,6 +2177,7 @@ public class AppController implements Initializable {
new KeyFrame(Duration.ZERO, new KeyValue(statusBar.progressProperty(), 0)),
new KeyFrame(Duration.millis(event.getTimeMills()), e -> {
statusBar.setText("");
statusBar.setGraphic(null);
statusBar.setProgress(0);
}, new KeyValue(statusBar.progressProperty(), 1))
);
@@ -1984,7 +2222,9 @@ public class AppController implements Initializable {
@Subscribe
public void connectionFailed(ConnectionFailedEvent event) {
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
statusUpdated(new StatusEvent(status));
Hyperlink hyperlink = new Hyperlink("Server Preferences");
hyperlink.setOnAction(this::openServerPreferences);
statusUpdated(new StatusEvent(status, hyperlink));
serverToggleStopAnimation();
setTorIcon();
}
@@ -2038,7 +2278,7 @@ public class AppController implements Initializable {
@Subscribe
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) {
if(event.getWalletNodes() == null && !event.getWallet().hasTransactions()) {
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(-1);
@@ -2248,23 +2488,20 @@ public class AppController implements Initializable {
}
@Subscribe
public void sendAction(SendActionEvent event) {
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
}
@Subscribe
public void recieveAction(ReceiveActionEvent event) {
selectTab(event.getWallet());
}
@Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) {
public void childWalletsAdded(ChildWalletsAddedEvent 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);
for(Wallet childWallet : event.getChildWallets()) {
addWalletTab(storage, childWallet);
}
}
@Subscribe
@@ -2272,6 +2509,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
exportWallet.setDisable(true);
}
}
@@ -2280,6 +2518,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
exportWallet.setDisable(!event.getWallet().isValid());
}
}
}
@@ -9,13 +9,17 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.control.TextUtils;
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.paynym.PayNymService;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
@@ -73,16 +77,26 @@ public class AppServices {
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
private static AppServices INSTANCE;
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
private final SorobanServices sorobanServices = new SorobanServices();
private static PayNymService payNymService;
private final MainApp application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
private TrayManager trayManager;
private static Image windowIcon;
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
private ExchangeSource.RatesService ratesService;
@@ -103,7 +117,7 @@ public class AppServices {
private static Map<Integer, Double> targetBlockFeeRates;
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
@@ -151,6 +165,7 @@ public class AppServices {
this.application = application;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
}
public void start() {
@@ -220,6 +235,11 @@ public class AppServices {
versionCheckService.cancel();
}
if(payNymService != null) {
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
shutdownService.start();
}
if(Tor.getDefault() != null) {
Tor.getDefault().shutdown();
}
@@ -471,6 +491,30 @@ public class AppServices {
return get().whirlpoolServices;
}
public static SorobanServices getSorobanServices() {
return get().sorobanServices;
}
public static PayNymService getPayNymService() {
if(payNymService == null) {
HostAndPort torProxy = getTorProxy();
payNymService = new PayNymService(torProxy);
} else {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
payNymService.setTorProxy(getTorProxy());
}
}
return payNymService;
}
public static HostAndPort getTorProxy() {
return AppServices.isTorRunning() ?
HostAndPort.fromParts("localhost", TorService.PROXY_PORT) :
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
}
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
@@ -482,9 +526,9 @@ public class AppServices {
stage.setTitle("Sparrow");
stage.setMinWidth(650);
stage.setMinHeight(730);
stage.setMinHeight(708);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
stage.getIcons().add(getWindowIcon());
appController.initializeView();
stage.show();
@@ -495,6 +539,14 @@ public class AppServices {
}
}
private static Image getWindowIcon() {
if(windowIcon == null) {
windowIcon = new Image(MainApp.class.getResourceAsStream("/image/sparrow-icon.png"));
}
return windowIcon;
}
public static boolean isReducedWindowHeight(Node node) {
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
}
@@ -535,6 +587,18 @@ public class AppServices {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public WalletTransaction getCreatedTransaction(Set<BlockTransactionHashIndex> utxos) {
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
for(WalletTabData walletTabData : walletTabDataList) {
if(walletTabData.getWalletForm().getCreatedWalletTransaction() != null && utxos.equals(walletTabData.getWalletForm().getCreatedWalletTransaction().getSelectedUtxos().keySet())) {
return walletTabData.getWalletForm().getCreatedWalletTransaction();
}
}
}
return 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);
@@ -569,11 +633,22 @@ public class AppServices {
return latestBlockHeader;
}
public static Double getDefaultFeeRate() {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
}
public static Double getMinimumFeeRate() {
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
public static Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
public static TreeMap<Date, Set<MempoolRateSize>> getMempoolHistogram() {
return mempoolHistogram;
}
@@ -584,6 +659,9 @@ public class AppServices {
}
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
Date yesterday = Date.from(LocalDateTime.now().minusDays(1).atZone(ZoneId.systemDefault()).toInstant());
mempoolHistogram.keySet().removeIf(date -> date.before(yesterday));
}
public static Double getMinimumRelayFeeRate() {
@@ -611,6 +689,12 @@ public class AppServices {
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
AppServices.clearTransactionHistoryCache(childWallet);
}
}
}
public static boolean isWalletFile(File file) {
@@ -650,8 +734,9 @@ public class AppServices {
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3) {
alert.getDialogPane().setPrefHeight(200 + lines.length * 20);
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}
moveToActiveWindowScreen(alert);
@@ -660,7 +745,7 @@ public class AppServices {
public static void setStageIcon(Window window) {
Stage stage = (Stage)window;
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow-large.png")));
stage.getIcons().add(getWindowIcon());
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
@@ -760,8 +845,6 @@ public class AppServices {
Platform.runLater(() -> {
if("bitcoin".equals(uri.getScheme())) {
openBitcoinUri(uri);
} else if("aopp".equals(uri.getScheme())) {
openAddressOwnershipProof(uri);
}
});
}
@@ -796,20 +879,6 @@ public class AppServices {
}
}
public static void openAddressOwnershipProof(URI uri) {
try {
Aopp aopp = new Aopp(uri);
Wallet wallet = selectWallet(aopp.getScriptType(), "send proof of address");
if(wallet != null) {
EventManager.get().post(new ReceiveActionEvent(wallet));
Platform.runLater(() -> EventManager.get().post(new ReceiveProofEvent(wallet, aopp)));
}
} catch(Exception e) {
showErrorDialog("Not a valid AOPP URI", e.getMessage());
}
}
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
Wallet wallet = null;
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
@@ -846,6 +915,7 @@ public class AppServices {
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer();
}
@Subscribe
@@ -8,7 +8,6 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.Bwt;
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
@@ -31,7 +30,7 @@ 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.5.2";
public static final String APP_VERSION = "1.6.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";
@@ -77,7 +76,8 @@ public class MainApp extends Application {
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)].getUrl());
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
}
}
}
@@ -46,7 +46,9 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
if(!childWallet.isNested()) {
existingIndexes.add(childWallet.getAccountIndex());
}
}
List<StandardAccount> availableAccounts = new ArrayList<>();
@@ -9,6 +9,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
@@ -35,6 +36,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
@@ -48,7 +50,8 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
}
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimationUtil {
public static Timeline getSlowFadeOut(Node node, Duration duration, double fromValue, int numIncrements) {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double percent = ((double)numIncrements - i - 1) / numIncrements;
double opacity = percent * fromValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public static Timeline getPulse(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline pulseTimeline = getFade(node, duration, fromValue, toValue, numIncrements);
pulseTimeline.setCycleCount(Animation.INDEFINITE);
pulseTimeline.setAutoReverse(true);
return pulseTimeline;
}
public static Timeline getFade(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double percent = ((double) numIncrements - i - 1) / numIncrements; //From 99% to 0%
double opacity = (percent * (fromValue - toValue)) + toValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public record AnimatedNode (Node node, Timeline timeline) {}
}
@@ -1,20 +1,26 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.collect.Lists;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BalanceChart extends LineChart<Number, Number> {
private static final int MAX_VALUES = 500;
private XYChart.Series<Number, Number> balanceSeries;
private TransactionEntry selectedEntry;
@@ -37,7 +43,7 @@ public class BalanceChart extends LineChart<Number, Number> {
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
balanceSeries.getData().clear();
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
List<Data<Number, Number>> balanceDataList = getTransactionEntries(walletTransactionsEntry)
.map(entry -> (TransactionEntry)entry)
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
@@ -74,6 +80,24 @@ public class BalanceChart extends LineChart<Number, Number> {
}
}
private Stream<Entry> getTransactionEntries(WalletTransactionsEntry walletTransactionsEntry) {
int total = walletTransactionsEntry.getChildren().size();
if(walletTransactionsEntry.getChildren().size() <= MAX_VALUES) {
return walletTransactionsEntry.getChildren().stream();
}
int bucketSize = total / MAX_VALUES;
List<List<Entry>> buckets = Lists.partition(walletTransactionsEntry.getChildren(), bucketSize);
List<Entry> reducedEntries = new ArrayList<>(MAX_VALUES);
for(List<Entry> bucket : buckets) {
long max = bucket.stream().mapToLong(entry -> Math.abs(entry.getValue())).max().orElse(0);
Entry bucketEntry = bucket.stream().filter(entry -> entry.getValue() == max || entry.getValue() == -max).findFirst().orElseThrow();
reducedEntries.add(bucketEntry);
}
return reducedEntries.stream();
}
public void select(TransactionEntry transactionEntry) {
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
for(Node selectedSymbol : selectedSymbols) {
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.SendController;
import com.sparrowwallet.sparrow.AppServices;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
@@ -28,7 +28,7 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
Integer targetBlocks = targetBlocksIter.next();
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
@@ -10,6 +10,7 @@ import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.Region;
import javafx.util.Duration;
import org.controlsfx.tools.Platform;
import java.text.DecimalFormat;
@@ -24,6 +25,7 @@ class CoinCell extends TreeTableCell<Entry, Number> {
public CoinCell() {
super();
tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(500));
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
@@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
@@ -15,7 +16,7 @@ import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class CoinLabel extends CopyableLabel {
public class CoinLabel extends Label {
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
@@ -28,7 +29,6 @@ public class CoinLabel extends CopyableLabel {
public CoinLabel(String text) {
super(text);
BTC_FORMAT.setMaximumFractionDigits(8);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
@@ -77,7 +77,7 @@ public class CoinLabel extends CopyableLabel {
private class CoinContextMenu extends ContextMenu {
public CoinContextMenu() {
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
copySatsValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
@@ -86,7 +86,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.setBirthDate(optDate.get());
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
@@ -12,13 +13,22 @@ import org.controlsfx.control.textfield.CustomTextField;
public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
private boolean initialized;
private boolean comboShowing;
public ComboBoxTextField() {
super();
getStyleClass().add("combo-text-field");
setupCopyButtonField(super.rightProperty());
setupComboButtonField(super.rightProperty());
disabledProperty().addListener((observable, oldValue, newValue) -> {
if(comboProperty.isNotNull().get()) {
comboProperty.get().setVisible(!newValue);
}
});
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
private void setupComboButtonField(ObjectProperty<Node> rightProperty) {
Region showComboButton = new Region();
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
StackPane showComboButtonPane = new StackPane(showComboButton);
@@ -26,7 +36,21 @@ public class ComboBoxTextField extends CustomTextField {
showComboButtonPane.setCursor(Cursor.DEFAULT);
showComboButtonPane.setOnMouseReleased(e -> {
if(comboProperty.isNotNull().get()) {
comboProperty.get().show();
if(comboShowing) {
comboProperty.get().hide();
} else {
comboProperty.get().show();
}
comboShowing = !comboShowing;
if(!initialized) {
comboProperty.get().valueProperty().addListener((observable, oldValue, newValue) -> {
comboShowing = false;
Platform.runLater(() -> comboProperty.get().getSelectionModel().clearSelection());
});
initialized = true;
}
}
});
@@ -86,10 +86,10 @@ public class ConfirmationProgressIndicator extends StackPane {
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
sequence.getChildren().add(upTickLineTimeline);
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(10), confirmationGroup);
groupFadeOut.setFromValue(1);
groupFadeOut.setToValue(0);
Timeline groupFadeOut = AnimationUtil.getSlowFadeOut(confirmationGroup, Duration.minutes(10), 1.0, 10);
sequence.getChildren().add(groupFadeOut);
confirmationsProperty().unbind();
}
sequence.play();
@@ -0,0 +1,94 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.util.Locale;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
public CopyableCoinLabel() {
this("Unknown");
}
public CopyableCoinLabel(String text) {
super(text);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
public final LongProperty valueProperty() {
return valueProperty;
}
public final long getValue() {
return valueProperty.get();
}
public final void setValue(long value) {
this.valueProperty.set(value);
}
public void refresh() {
refresh(Config.get().getBitcoinUnit());
}
public void refresh(BitcoinUnit bitcoinUnit) {
setValueAsText(getValue(), bitcoinUnit);
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
BitcoinUnit unit = bitcoinUnit;
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue);
setText(btcValue);
} else {
tooltip.setText(btcValue);
setText(satsValue);
}
}
private class CoinContextMenu extends ContextMenu {
public CoinContextMenu() {
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
copySatsValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Long.toString(getValue()));
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
}
}
}
@@ -1,13 +1,19 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
@@ -16,10 +22,36 @@ import org.controlsfx.control.textfield.CustomTextField;
public class CopyableTextField extends CustomTextField {
private static final Duration FADE_DURATION = Duration.millis(350);
private final ChangeListener<String> selectionListener = (textObservable, textOldValue, textNewValue) -> {
if(!textNewValue.isEmpty()) {
deselect();
}
};
private final EventHandler<MouseEvent> copyHandler = event -> {
ClipboardContent content = new ClipboardContent();
content.putString(getCopyText());
Clipboard.getSystemClipboard().setContent(content);
Tooltip tooltip = new Tooltip("Copied!");
tooltip.show(this, event.getScreenX(), event.getScreenY());
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide()));
timeline.play();
};
public CopyableTextField() {
super();
getStyleClass().add("copyable-text-field");
setupCopyButtonField(super.rightProperty());
editableProperty().addListener((observable, oldValue, editable) -> {
if(!editable) {
setOnMouseClicked(copyHandler);
selectedTextProperty().addListener(selectionListener);
} else {
setOnMouseClicked(null);
selectedTextProperty().removeListener(selectionListener);
}
});
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
@@ -31,7 +63,7 @@ public class CopyableTextField extends CustomTextField {
copyButtonPane.setCursor(Cursor.DEFAULT);
copyButtonPane.setOnMouseReleased(e -> {
ClipboardContent content = new ClipboardContent();
content.putString(getText());
content.putString(getCopyText());
Clipboard.getSystemClipboard().setContent(content);
});
@@ -61,4 +93,8 @@ public class CopyableTextField extends CustomTextField {
}
});
}
protected String getCopyText() {
return getText();
}
}
@@ -7,6 +7,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.util.Duration;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -46,6 +47,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
}
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
int height = utxoEntry.getHashIndex().getHeight();
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
setTooltip(tooltip);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
@@ -79,7 +80,7 @@ public class DescriptorArea extends CodeArea {
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
content.putString(OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyOutputDescriptor);
@@ -26,7 +26,6 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
@@ -332,7 +331,7 @@ public class DevicePane extends TitledDescriptionPane {
VBox vBox = new VBox();
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
pinField = new ViewPasswordField();
Platform.runLater(() -> pinField.requestFocus());
enterPinButton = new Button("Enter PIN");
enterPinButton.setDefaultButton(true);
@@ -372,7 +371,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private Node getPassphraseEntry() {
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
CustomPasswordField passphraseField = new ViewPasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
passphraseField.setOnAction(event -> {
@@ -625,7 +624,7 @@ public class DevicePane extends TitledDescriptionPane {
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
signButton.setDisable(false);
});
setDescription("Signing...");
@@ -697,17 +696,17 @@ public class DevicePane extends TitledDescriptionPane {
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
walletDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores);
accountDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(accountDiscoveryService.getValue());
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
});
walletDiscoveryService.setOnFailed(event -> {
accountDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
walletDiscoveryService.start();
accountDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
@@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
@@ -19,6 +20,7 @@ import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import javafx.util.Duration;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
@@ -69,10 +71,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltip(transactionEntry));
setTooltip(tooltip);
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
tooltip.setOnShowing(event -> {
tooltip.setText(getTooltip(transactionEntry));
});
}
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
@@ -106,20 +116,25 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
actionBox.getChildren().add(receiveButton);
actionBox.getStyleClass().add("cell-actions");
if(canSignMessage(nodeEntry.getWallet())) {
if(!nodeEntry.getNode().getWallet().isBip47()) {
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
actionBox.getChildren().add(receiveButton);
}
if(canSignMessage(nodeEntry.getNode())) {
Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> {
@@ -139,12 +154,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
@@ -161,7 +178,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
actionBox.getChildren().add(spendUtxoButton);
}
setGraphic(actionBox);
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
}
}
}
@@ -267,15 +284,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(Wallet wallet) {
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@@ -326,6 +345,30 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private String getTooltip(TransactionEntry transactionEntry) {
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
if(!AppServices.getMempoolHistogram().isEmpty()) {
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
double vSize = transactionEntry.getBlockTransaction().getTransaction().getVirtualSize();
double feeRate = transactionEntry.getBlockTransaction().getFee() / vSize;
long vSizefromTip = rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
String amount = vSizefromTip + " vB";
if(vSizefromTip > 1000 * 1000) {
amount = String.format("%.2f", (double)vSizefromTip / (1000 * 1000)) + " MvB";
} else if(vSizefromTip > 1000) {
amount = String.format("%.2f", (double)vSizefromTip / 1000) + " kvB";
}
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB (" + amount + " from tip)";
}
tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled");
}
return tooltip;
}
private static Glyph getViewTransactionGlyph() {
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
@@ -464,16 +507,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
hide();
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
getItems().add(receiveToAddress);
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
hide();
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
getItems().add(receiveToAddress);
}
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
@@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
protected ButtonBase importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
private final boolean scannable;
private final boolean fileFormatAvailable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl);
this.importer = importer;
this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable;
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
@@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
@Override
protected Control createButton() {
if(scannable) {
if(scannable && fileFormatAvailable) {
ToggleButton scanButton = new ToggleButton("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
@@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(scanButton, fileButton);
return segmentedButton;
} else if(scannable) {
importButton = new Button("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
importButton.setGraphic(cameraGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importQR();
});
return importButton;
} else {
importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
@@ -146,6 +158,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
try {
importFile(importer.getName(), null, null);
} catch(ImportException e) {
log.error("Error importing QR", e);
setError("Import Error", e.getMessage());
}
} else if(result.outputDescriptor != null) {
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
try {
importFile(importer.getName(), null, null);
} catch(ImportException e) {
log.error("Error importing QR", e);
setError("Import Error", e.getMessage());
}
} else if(result.payload != null) {
@@ -165,6 +186,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
} else if(result.exception != null) {
log.error("Error importing QR", result.exception);
setError("Import Error", result.exception.getMessage());
} else {
setError("Import Error", null);
setExpanded(true);
}
}
}
@@ -190,7 +214,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
private final WalletImport importer;
public FileWalletImportPane(WalletImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
this.importer = importer;
}
@@ -22,6 +22,7 @@ import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +40,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private byte[] fileBytes;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.importer = importer;
}
@@ -97,13 +98,29 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public ScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
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.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@@ -130,7 +147,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, scriptTypeComboBox, helpLabel, region, importFileButton);
contentBox.getChildren().addAll(label, fieldBox, region, importFileButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
@@ -25,7 +25,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
this.passphrase = new ViewPasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
@@ -92,18 +92,15 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
});
getItems().add(copyLabel);
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(content instanceof String) {
MenuItem pasteLabel = new MenuItem("Paste Label");
pasteLabel.setOnAction(AE -> {
hide();
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(currentContent instanceof String) {
entry.labelProperty().set((String)currentContent);
}
});
getItems().add(pasteLabel);
}
MenuItem pasteLabel = new MenuItem("Paste Label");
pasteLabel.setOnAction(AE -> {
hide();
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(currentContent instanceof String) {
entry.labelProperty().set((String)currentContent);
}
});
getItems().add(pasteLabel);
}
}
}
@@ -1,8 +1,8 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
@@ -79,9 +79,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
});
long previousFeeRate = 0;
for(Long feeRate : SendController.FEE_RATES_RANGE) {
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName(feeRate + "+ vB");
series.setName(feeRate + "+ sats/vB");
long seriesTotalVSize = 0;
for(Date date : periodRateSizes.keySet()) {
@@ -89,13 +89,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
if(walletNode != null) {
checkWalletSigning(walletNode.getWallet());
}
if(wallet != null) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
checkWalletSigning(wallet);
}
this.wallet = wallet;
@@ -103,6 +102,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
@@ -120,6 +120,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
fieldset.setSpacing(10);
Field addressField = new Field();
addressField.setText("Address:");
@@ -129,7 +130,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
addressField.getInputs().add(address);
if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString());
address.setText(walletNode.getAddress().toString());
}
Field messageField = new Field();
@@ -262,6 +263,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
});
}
private void checkWalletSigning(Wallet wallet) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
}
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
@@ -299,20 +309,21 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(wallet.containsPrivateKeys()) {
if(wallet.isEncrypted()) {
//Note we can expect a single keystore due to the check in the constructor
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
signUnencryptedKeystore(wallet);
signUnencryptedKeystore(signingWallet);
}
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(signingWallet);
}
}
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
@@ -402,7 +413,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
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());
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(walletNode.getWallet().copy(), password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
@@ -10,7 +10,7 @@ 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.animation.FadeTransition;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.util.Duration;
@@ -84,13 +84,13 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
FadeTransition ft = new FadeTransition(Duration.millis(ERROR_DISPLAY_MILLIS - elapsed), failGlyph);
ft.setFromValue(1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS));
ft.setToValue(0.0);
ft.setOnFinished(event -> {
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
timeline.setOnFinished(event -> {
setTooltip(null);
});
ft.play();
timeline.play();
} else {
setGraphic(null);
setTooltip(null);
@@ -0,0 +1,72 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.List;
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
public MnemonicKeystoreDisplayPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
showWordList(keystore.getSeed());
}
private void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size()));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
}
}
protected Node getMnemonicWordsEntry(int numWords) {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords / 3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
return stackPane;
}
}
@@ -3,30 +3,22 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
import javafx.geometry.Insets;
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;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.controlsfx.tools.Borders;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
@@ -35,34 +27,24 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.NoSuchAlgorithmException;
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 {
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected final Wallet wallet;
private final KeystoreMnemonicImport importer;
private SplitMenuButton enterMnemonicButton;
private SplitMenuButton importButton;
private TilePane wordsPane;
private Button generateButton;
private Label validLabel;
private Label invalidLabel;
private Button calculateButton;
private Button backButton;
private Button nextButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
private SimpleListProperty<String> wordEntriesProperty;
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
private IntegerProperty defaultWordSizeProperty;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
@@ -70,46 +52,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
buttonBox.getChildren().add(importButton);
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
showWordList(keystore.getSeed());
}
@Override
protected Control createButton() {
createEnterMnemonicButton();
return enterMnemonicButton;
}
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Enter 24 Words");
defaultWordSizeProperty = new SimpleIntegerProperty(24);
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
enterMnemonicButton.setText("Enter " + newValue + " Words");
});
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(defaultWordSizeProperty.get());
});
int[] numberWords = new int[] {24, 21, 18, 15, 12};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words");
final int words = numberWords[i];
item.setOnAction(event -> {
defaultWordSizeProperty.set(words);
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
@@ -135,162 +77,81 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importButton.setVisible(false);
}
private void enterMnemonic(int numWords) {
protected void enterMnemonic(int numWords) {
generatedMnemonicCode = null;
setDescription("Enter mnemonic word list");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords, false));
setExpanded(true);
super.enterMnemonic(numWords);
}
private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) {
VBox vBox = new VBox();
vBox.setSpacing(10);
protected List<Node> createLeftButtons() {
generateButton = new Button("Generate New");
generateButton.setOnAction(event -> {
generateNew();
});
generateButton.managedProperty().bind(generateButton.visibleProperty());
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
return List.of(generateButton);
}
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
protected List<Node> createRightButtons() {
confirmButton = new Button("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
vBox.getChildren().add(wordsPane);
backButton = new Button("Back");
backButton.setOnAction(event -> {
displayMnemonicCode();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
nextButton = new Button("Confirm Backup...");
nextButton.setOnAction(event -> {
confirmRecord();
});
nextButton.managedProperty().bind(nextButton.visibleProperty());
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
nextButton.setVisible(false);
nextButton.setDefaultButton(true);
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 26, 0, 10));
return List.of(backButton, nextButton, confirmButton, calculateButton);
}
generateButton = new Button("Generate New");
generateButton.setOnAction(event -> {
generateNew();
});
generateButton.managedProperty().bind(generateButton.visibleProperty());
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
buttonPane.getChildren().add(generateButton);
AnchorPane.setLeftAnchor(generateButton, 0.0);
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
backButton = new Button("Back");
backButton.setOnAction(event -> {
displayMnemonicCode();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
nextButton = new Button("Confirm Backup...");
nextButton.setOnAction(event -> {
confirmRecord();
});
nextButton.managedProperty().bind(nextButton.visibleProperty());
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
nextButton.setVisible(false);
nextButton.setDefaultButton(true);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
boolean validChecksum = false;
for(String word : wordEntryList) {
if(!word.isEmpty()) {
empty = false;
}
if(!WordEntry.isValid(word)) {
validWords = false;
}
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
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);
}
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
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);
}
}
}
generateButton.setVisible(empty && generatedMnemonicCode == null);
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
});
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(backButton, nextButton, confirmButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
}
}
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
return stackPane;
generateButton.setVisible(empty && generatedMnemonicCode == null);
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
}
private void generateNew() {
@@ -362,7 +223,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void confirmBackup() {
setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
@@ -415,145 +276,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
}
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
setAlignment(Pos.CENTER_RIGHT);
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
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();
// if text was added, fix the text to fit the requirements
if(!text.isEmpty()) {
String newText = text.replace(" ", "").toLowerCase();
int carretPos = change.getCaretPosition() - text.length() + newText.length();
change.setText(newText);
// fix caret position based on difference in originally added text and fixed text
change.selectRange(carretPos, carretPos);
}
return change;
});
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
nextField.requestFocus();
}
});
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
));
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
wordEntryList.set(wordNumber, newValue);
});
this.getChildren().addAll(label, wordField);
}
public TextField getEditor() {
return wordField;
}
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
private static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
public WordlistSuggestionProvider(List<String> wordList) {
this.wordList = wordList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
}
}
return suggestions;
}
}
private class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
public PassphraseEntry() {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label passphraseLabel = new Label("Passphrase:");
passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
HelpLabel helpLabel = new HelpLabel();
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
public TextField getEditor() {
return passphraseField;
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
@@ -591,30 +313,4 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return contentBox;
}
private void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size(), true));
setExpanded(true);
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("valid-checksum");
validGlyph.setFontSize(12);
return validGlyph;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("invalid-checksum");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}
@@ -0,0 +1,344 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
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;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MnemonicKeystorePane extends TitledDescriptionPane {
protected SplitMenuButton enterMnemonicButton;
protected TilePane wordsPane;
protected Label validLabel;
protected Label invalidLabel;
protected SimpleListProperty<String> wordEntriesProperty;
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
protected IntegerProperty defaultWordSizeProperty;
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
super(title, description, content, imageUrl);
}
@Override
protected Control createButton() {
createEnterMnemonicButton();
return enterMnemonicButton;
}
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Use 24 Words");
defaultWordSizeProperty = new SimpleIntegerProperty(24);
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
enterMnemonicButton.setText("Use " + newValue + " Words");
});
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(defaultWordSizeProperty.get());
});
int[] numberWords = new int[] {24, 21, 18, 15, 12};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
final int words = numberWords[i];
item.setOnAction(event -> {
defaultWordSizeProperty.set(words);
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
protected void enterMnemonic(int numWords) {
setDescription("Generate new or enter existing");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords));
setExpanded(true);
}
protected Node getMnemonicWordsEntry(int numWords) {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 26, 0, 10));
HBox leftBox = new HBox(10);
leftBox.getChildren().addAll(createLeftButtons());
buttonPane.getChildren().add(leftBox);
AnchorPane.setLeftAnchor(leftBox, 0.0);
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
boolean validChecksum = false;
for(String word : wordEntryList) {
if(!word.isEmpty()) {
empty = false;
}
if(!WordEntry.isValid(word)) {
validWords = false;
}
}
onWordChange(empty, validWords, validChecksum);
});
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(createRightButtons());
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
Platform.runLater(() -> wordEntries.get(0).getEditor().requestFocus());
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
return stackPane;
}
protected List<Node> createLeftButtons() {
return Collections.emptyList();
}
protected List<Node> createRightButtons() {
return Collections.emptyList();
}
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
//nothing by default
}
protected static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
setAlignment(Pos.CENTER_RIGHT);
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
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();
// if text was added, fix the text to fit the requirements
if(!text.isEmpty()) {
String newText = text.replace(" ", "").toLowerCase();
int carretPos = change.getCaretPosition() - text.length() + newText.length();
change.setText(newText);
// fix caret position based on difference in originally added text and fixed text
change.selectRange(carretPos, carretPos);
}
return change;
});
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
nextField.requestFocus();
}
});
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
));
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
wordEntryList.set(wordNumber, newValue);
});
this.getChildren().addAll(label, wordField);
}
public TextField getEditor() {
return wordField;
}
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
public WordlistSuggestionProvider(List<String> wordList) {
this.wordList = wordList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
}
}
return suggestions;
}
}
protected class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
public PassphraseEntry() {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label passphraseLabel = new Label("Passphrase:");
passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
HelpLabel helpLabel = new HelpLabel();
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
public TextField getEditor() {
return passphraseField;
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("success");
validGlyph.setFontSize(12);
return validGlyph;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("failure");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}
@@ -0,0 +1,220 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.crypto.ChildNumber;
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.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
private static final Logger log = LoggerFactory.getLogger(MnemonicWalletKeystoreImportPane.class);
private final KeystoreMnemonicImport importer;
private Button discoverButton;
private Button importButton;
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.importer = importer;
}
@Override
protected void enterMnemonic(int numWords) {
super.enterMnemonic(numWords);
setDescription("Enter word list");
}
@Override
protected List<Node> createRightButtons() {
discoverButton = new Button("Discover Wallet");
discoverButton.setDisable(true);
discoverButton.setDefaultButton(true);
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setOnAction(event -> {
discoverWallet();
});
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setTooltip(new Tooltip("Look for existing transactions from the provided word list"));
discoverButton.visibleProperty().bind(AppServices.onlineProperty());
importButton = new Button("Import Wallet");
importButton.setDisable(true);
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
importButton.setOnAction(event -> {
setContent(getScriptTypeEntry());
setExpanded(true);
});
return List.of(discoverButton, importButton);
}
@Override
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
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);
}
}
}
discoverButton.setDisable(!validChecksum || !AppServices.isConnected());
importButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
}
private void discoverWallet() {
discoverButton.setDisable(true);
discoverButton.setMaxHeight(discoverButton.getHeight());
ProgressIndicator progressIndicator = new ProgressIndicator(0);
progressIndicator.getStyleClass().add("button-progress");
discoverButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
derivations.add(List.of(new ChildNumber(0, true)));
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
for(List<ChildNumber> derivation : derivations) {
try {
Wallet wallet = getWallet(scriptType, derivation);
wallets.add(wallet);
} catch(ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
errorMessage = "Invalid word list - checksum incorrect";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage + ".");
discoverButton.setDisable(!AppServices.isConnected());
}
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
walletDiscoveryService.setOnSucceeded(successEvent -> {
discoverButton.setGraphic(null);
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
if(optWallet.isPresent()) {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
}
}
});
walletDiscoveryService.setOnFailed(failedEvent -> {
discoverButton.setGraphic(null);
log.error("Failed to discover wallets", failedEvent.getSource().getException());
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
});
walletDiscoveryService.start();
}
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
Wallet wallet = new Wallet("");
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
return wallet;
}
private Node getScriptTypeEntry() {
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public ScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
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.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importMnemonicButton = new Button("Import");
importMnemonicButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
ScriptType scriptType = scriptTypeComboBox.getValue();
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
EventManager.get().post(new WalletImportEvent(wallet));
} catch(ImportException e) {
log.error("Error importing mnemonic", e);
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, fieldBox, region, importMnemonicButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -0,0 +1,138 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class PayNymAvatar extends StackPane {
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
public PayNymAvatar() {
super();
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
if(paymentCode == null) {
getChildren().clear();
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
String cacheId = getCacheId(paymentCode, getPrefWidth());
if(paymentCodeCache.containsKey(cacheId)) {
setImage(paymentCodeCache.get(cacheId));
} else {
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
payNymAvatarService.setOnRunning(runningEvent -> {
getChildren().clear();
});
payNymAvatarService.setOnSucceeded(successEvent -> {
setImage(payNymAvatarService.getValue());
});
payNymAvatarService.setOnFailed(failedEvent -> {
log.error("Error", failedEvent.getSource().getException());
});
payNymAvatarService.start();
}
}
});
}
private void setImage(Image image) {
getChildren().clear();
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
circle.setFill(new ImagePattern(image));
getChildren().add(circle);
}
public PaymentCode getPaymentCode() {
return paymentCodeProperty.get();
}
public ObjectProperty<PaymentCode> paymentCodeProperty() {
return paymentCodeProperty;
}
public void setPaymentCode(PaymentCode paymentCode) {
this.paymentCodeProperty.set(paymentCode);
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
}
public void clearPaymentCode() {
this.paymentCodeProperty.set(null);
}
private static String getCacheId(PaymentCode paymentCode, double width) {
return paymentCode.toString();
}
private static class PayNymAvatarService extends Service<Image> {
private final PaymentCode paymentCode;
private final double width;
public PayNymAvatarService(PaymentCode paymentCode, double width) {
this.paymentCode = paymentCode;
this.width = width;
}
@Override
protected Task<Image> createTask() {
return new Task<>() {
@Override
protected Image call() throws Exception {
String paymentCodeStr = paymentCode.toString();
String cacheId = getCacheId(paymentCode, width);
Object lock = paymentCodeLoading.get(cacheId);
if(lock != null) {
synchronized(lock) {
if(paymentCodeCache.containsKey(cacheId)) {
return paymentCodeCache.get(cacheId);
}
}
} else {
lock = new Object();
paymentCodeLoading.put(cacheId, lock);
}
synchronized(lock) {
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
Proxy proxy = AppServices.getProxy();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
paymentCodeCache.put(cacheId, image);
return image;
} catch(Exception e) {
log.debug("Error loading PayNym avatar", e);
throw e;
} finally {
lock.notifyAll();
}
}
}
};
}
}
}
@@ -0,0 +1,94 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.paynym.PayNym;
import com.sparrowwallet.sparrow.paynym.PayNymController;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.Glyph;
public class PayNymCell extends ListCell<PayNym> {
private final PayNymController payNymController;
public PayNymCell(PayNymController payNymController) {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("paynym-cell");
setPrefHeight(50);
this.payNymController = payNymController;
}
@Override
protected void updateItem(PayNym payNym, boolean empty) {
super.updateItem(payNym, empty);
getStyleClass().remove("unlinked");
if(empty || payNym == null) {
setText(null);
setGraphic(null);
} else {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(5, 5,5, 5));
PayNymAvatar payNymAvatar = new PayNymAvatar();
payNymAvatar.setPrefWidth(30);
payNymAvatar.setPrefHeight(30);
payNymAvatar.setPaymentCode(payNym.paymentCode());
HBox labelBox = new HBox();
labelBox.setAlignment(Pos.CENTER);
Label label = new Label(payNym.nymName(), payNymAvatar);
label.setGraphicTextGap(10);
labelBox.getChildren().add(label);
pane.setLeft(labelBox);
if(getListView().getUserData() == Boolean.TRUE) {
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
Button button = new Button("Add Contact");
hBox.getChildren().add(button);
pane.setRight(hBox);
button.setOnAction(event -> {
button.setDisable(true);
payNymController.followPayNym(payNym.paymentCode());
});
} else if(payNymController != null) {
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
pane.setRight(hBox);
if(payNymController.isLinked(payNym)) {
Label linkedLabel = new Label("Linked", getLinkGlyph());
linkedLabel.setTooltip(new Tooltip("You can send non-collaboratively to this contact."));
hBox.getChildren().add(linkedLabel);
} else {
Button linkButton = new Button("Link Contact", getLinkGlyph());
linkButton.setTooltip(new Tooltip("Create a transaction that will enable you to send non-collaboratively to this contact."));
hBox.getChildren().add(linkButton);
linkButton.setOnAction(event -> {
linkButton.setDisable(true);
payNymController.linkPayNym(payNym);
});
if(payNymController.isSelectLinkedOnly()) {
getStyleClass().add("unlinked");
}
}
}
setText(null);
setGraphic(pane);
}
}
public static Glyph getLinkGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LINK);
failGlyph.setFontSize(12);
return failGlyph;
}
}
@@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.bip47.PaymentCode;
public class PaymentCodeTextField extends CopyableTextField {
private String paymentCodeStr;
public void setPaymentCode(PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
setPaymentCodeString();
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
setPaymentCodeString();
}
private void setPaymentCodeString() {
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
setText(abbrevPcode);
}
@Override
protected String getCopyText() {
return paymentCodeStr;
}
}
@@ -0,0 +1,345 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.Files;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
public class PrivateKeySweepDialog extends Dialog<Transaction> {
private static final Logger log = LoggerFactory.getLogger(PrivateKeySweepDialog.class);
private final TextArea key;
private final ComboBox<ScriptType> keyScriptType;
private final CopyableLabel keyAddress;
private final ComboBoxTextField toAddress;
private final ComboBox<Wallet> toWallet;
public PrivateKeySweepDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText("Sweep Private Key");
Image image = new Image("image/seed.png", 50, 50, false, false);
if(!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
fieldset.setSpacing(10);
Field keyField = new Field();
keyField.setText("Private Key:");
key = new TextArea();
key.setWrapText(true);
key.setPromptText("Wallet Import Format (WIF)");
key.setPrefRowCount(2);
key.getStyleClass().add("fixed-width");
HBox keyBox = new HBox(5);
VBox keyButtonBox = new VBox(5);
Button scanKey = new Button("", getGlyph(FontAwesome5.Glyph.CAMERA));
scanKey.setOnAction(event -> scanPrivateKey());
Button readKey = new Button("", getGlyph(FontAwesome5.Glyph.FILE_IMPORT));
readKey.setOnAction(event -> readPrivateKey());
keyButtonBox.getChildren().addAll(scanKey, readKey);
keyBox.getChildren().addAll(key, keyButtonBox);
HBox.setHgrow(key, Priority.ALWAYS);
keyField.getInputs().add(keyBox);
Field keyScriptTypeField = new Field();
keyScriptTypeField.setText("Script Type:");
keyScriptType = new ComboBox<>();
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
keyScriptTypeField.getInputs().add(keyScriptType);
keyScriptType.setConverter(new StringConverter<ScriptType>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public ScriptType fromString(String string) {
return null;
}
});
Field addressField = new Field();
addressField.setText("Address:");
keyAddress = new CopyableLabel();
keyAddress.getStyleClass().add("fixed-width");
addressField.getInputs().add(keyAddress);
Field toAddressField = new Field();
toAddressField.setText("Sweep to:");
toAddress = new ComboBoxTextField();
toAddress.getStyleClass().add("fixed-width");
toWallet = new ComboBox<>();
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
toAddress.setComboProperty(toWallet);
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
StackPane stackPane = new StackPane();
stackPane.getChildren().addAll(toWallet, toAddress);
toAddressField.getInputs().add(stackPane);
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
ButtonType createButtonType = new javafx.scene.control.ButtonType("Create Transaction", ButtonBar.ButtonData.APPLY);
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType, createButtonType);
Button createButton = (Button) dialogPane.lookupButton(createButtonType);
createButton.setDefaultButton(true);
createButton.setDisable(true);
createButton.addEventFilter(ActionEvent.ACTION, event -> {
createTransaction();
event.consume();
});
key.textProperty().addListener((observable, oldValue, newValue) -> {
boolean isValidKey = isValidKey();
createButton.setDisable(!isValidKey || !isValidToAddress());
if(isValidKey) {
setFromAddress();
}
});
keyScriptType.valueProperty().addListener((observable, oldValue, newValue) -> {
if(isValidKey()) {
setFromAddress();
}
});
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
createButton.setDisable(!isValidKey() || !isValidToAddress());
});
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
if(selectedWallet != null) {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
});
keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> null);
dialogPane.setPrefWidth(680);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(key, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid private Key", !key.getText().isEmpty() && !isValidKey()));
validationSupport.registerValidator(toAddress, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !toAddress.getText().isEmpty() && !isValidToAddress()));
});
}
private boolean isValidKey() {
try {
DumpedPrivateKey privateKey = getPrivateKey();
return true;
} catch(Exception e) {
return false;
}
}
private DumpedPrivateKey getPrivateKey() {
return DumpedPrivateKey.fromBase58(key.getText());
}
private boolean isValidToAddress() {
try {
Address address = getToAddress();
return true;
} catch (InvalidAddressException e) {
return false;
}
}
private Address getToAddress() throws InvalidAddressException {
return Address.fromString(toAddress.getText());
}
private void setFromAddress() {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address address = scriptType.getAddress(privateKey.getKey());
keyAddress.setText(address.toString());
}
private void scanPrivateKey() {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> result = qrScanDialog.showAndWait();
if(result.isPresent() && result.get().payload != null) {
key.setText(result.get().payload);
}
}
private void readPrivateKey() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Private Key File");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
if(file.length() > 1024) {
AppServices.showErrorDialog("Invalid private key file", "This file does not contain a valid private key.");
return;
}
try {
key.setText(Files.asCharSource(file, StandardCharsets.UTF_8).read().trim());
} catch(IOException e) {
AppServices.showErrorDialog("Error reading private key file", e.getMessage());
}
}
}
private void createTransaction() {
try {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress();
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
});
addressUtxosService.setOnFailed(failedEvent -> {
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
});
addressUtxosService.start();
} catch(Exception e) {
log.error("Error creating sweep transaction", e);
}
}
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Transaction noFeeTransaction = new Transaction();
long total = 0;
for(TransactionOutput txOutput : txOutputs) {
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
total += txOutput.getValue();
}
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
noFeeTransaction.addOutput(sweepOutput);
Double feeRate = AppServices.getDefaultFeeRate();
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
fee++;
}
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
return;
}
Transaction transaction = new Transaction();
transaction.setVersion(2);
transaction.setLocktime(AppServices.getCurrentBlockHeight() == null ? 0 : AppServices.getCurrentBlockHeight());
for(TransactionInput txInput : noFeeTransaction.getInputs()) {
transaction.addInput(txInput);
}
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
PSBT psbt = new PSBT(transaction);
for(int i = 0; i < txOutputs.size(); i++) {
TransactionOutput utxoOutput = txOutputs.get(i);
TransactionInput txInput = transaction.getInputs().get(i);
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
psbtInput.setWitnessUtxo(utxoOutput);
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
}
if(txInput.getWitness() != null) {
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
}
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
return;
}
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
}
setResult(psbt.extractTransaction());
}
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
glyph.setFontSize(12);
return glyph;
}
}
@@ -0,0 +1,53 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ProgressIndicator;
import javafx.util.Duration;
public class ProgressTimer extends ProgressIndicator {
private final IntegerProperty secondsProperty = new SimpleIntegerProperty(60);
private Timeline timeline;
public ProgressTimer() {
super(0);
getStyleClass().add("progress-timer");
}
public void start() {
start(e -> {});
}
public void start(EventHandler<ActionEvent> onFinished) {
timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
new KeyFrame(Duration.seconds(getSeconds()), onFinished, new KeyValue(progressProperty(), 1)));
timeline.setCycleCount(1);
timeline.play();
}
public void stop() {
if(timeline != null) {
timeline.stop();
}
}
public int getSeconds() {
return secondsProperty.get();
}
public IntegerProperty secondsProperty() {
return secondsProperty;
}
public void setSeconds(int secondsProperty) {
this.secondsProperty.set(secondsProperty);
}
}
@@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -479,18 +481,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
List<Wallet> wallets = new ArrayList<>();
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
Wallet wallet = new Wallet();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
if(outputDescriptor.isMultisig()) {
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
}
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
wallet.setScriptType(outputDescriptor.getScriptType());
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
wallets.add(wallet);
}
@@ -499,7 +491,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
List<ScriptExpression> expressions = new ArrayList<>(scriptExpressions);
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG || expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG) {
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.COSIGNER) {
expressions.remove(expressions.size() - 1);
}
@@ -515,6 +509,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
return ScriptType.P2SH_P2WSH;
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2WSH;
} else if(List.of(ScriptExpression.TAPROOT).equals(expressions)) {
return ScriptType.P2TR;
}
throw new IllegalArgumentException("Unknown script of " + expressions);
@@ -0,0 +1,251 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.util.ArrayList;
import java.util.List;
public class SearchWalletDialog extends Dialog<Entry> {
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
private final List<WalletForm> walletForms;
private final TextField search;
private final CoinTreeTable results;
public SearchWalletDialog(List<WalletForm> walletForms) {
this.walletForms = walletForms;
if(walletForms.isEmpty()) {
throw new IllegalArgumentException("No wallets selected to search");
}
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText("Search Wallet");
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if(!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
fieldset.setSpacing(10);
Field searchField = new Field();
searchField.setText("Search:");
search = TextFields.createClearableTextField();
search.setPromptText("Label, address, value or transaction ID");
searchField.getInputs().add(search);
fieldset.getChildren().addAll(searchField);
form.getChildren().add(fieldset);
boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty());
results = new CoinTreeTable();
results.setShowRoot(false);
results.setPrefWidth(showWallet ? 950 : 850);
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
results.setPlaceholder(new Label("No results"));
if(showWallet) {
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName());
});
results.getColumns().add(walletColumn);
}
TreeTableColumn<Entry, String> typeColumn = new TreeTableColumn<>("Type");
typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType());
});
results.getColumns().add(typeColumn);
TreeTableColumn<Entry, Entry> entryCol = new TreeTableColumn<>("Date / Address / Output");
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
entryCol.setCellFactory(p -> new SearchEntryCell());
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
results.getColumns().add(entryCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new SearchLabelCell());
results.getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new CoinCell());
results.getColumns().add(amountCol);
vBox.getChildren().addAll(form, results);
dialogPane.setContent(vBox);
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
showButton.setDefaultButton(true);
showButton.setDisable(true);
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty()
|| walletForms.stream().map(WalletForm::getWallet).noneMatch(wallet -> wallet == results.getSelectionModel().getSelectedItem().getValue().getWallet()));
});
search.textProperty().addListener((observable, oldValue, newValue) -> {
searchWallet(newValue.toLowerCase());
});
setResizable(true);
Platform.runLater(search::requestFocus);
}
private void searchWallet(String searchText) {
List<Entry> matchingEntries = new ArrayList<>();
if(!searchText.isEmpty()) {
Long searchValue = null;
try {
searchValue = Math.abs(Long.parseLong(searchText));
} catch(NumberFormatException e) {
//ignore
}
for(WalletForm walletForm : walletForms) {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
for(Entry entry : walletTransactionsEntry.getChildren()) {
if(entry instanceof TransactionEntry transactionEntry) {
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) ||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
for(Entry entry : purposeEntry.getChildren()) {
if(entry instanceof NodeEntry nodeEntry) {
if(nodeEntry.getAddress().toString().contains(searchText) ||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
}
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
for(Entry entry : purposeEntry.getChildren()) {
if(entry instanceof NodeEntry nodeEntry) {
if(nodeEntry.getAddress().toString().contains(searchText) ||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
}
}
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
for(Entry entry : walletUtxosEntry.getChildren()) {
if(entry instanceof HashIndexEntry hashIndexEntry) {
if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) ||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
}
}
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
results.setRoot(rootItem);
}
private static class SearchWalletEntry extends Entry {
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
super(wallet, wallet.getName(), entries);
}
@Override
public Long getValue() {
return 0L;
}
@Override
public String getEntryType() {
return "Search Wallet Results";
}
@Override
public Function getWalletFunction() {
return null;
}
}
private static class SearchEntryCell extends EntryCell {
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
setContextMenu(null);
}
}
private static class SearchLabelCell extends LabelCell {
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
setContextMenu(null);
}
}
}
@@ -33,7 +33,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
Accordion keystoreAccordion = new Accordion();
scrollPane.setContent(keystoreAccordion);
MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore);
MnemonicKeystoreDisplayPane keystorePane = new MnemonicKeystoreDisplayPane(decryptedKeystore);
keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane);
@@ -34,4 +34,18 @@ public class TextUtils {
helper.setText(DEFAULT_TEXT);
return d;
}
public static double computeTextHeight(Font font, String text) {
helper.setText(text);
helper.setFont(font);
helper.setWrappingWidth(0.0D);
helper.setLineSpacing(0.0D);
double d = Math.ceil(helper.getLayoutBounds().getHeight());
helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
helper.setLineSpacing(DEFAULT_LINE_SPACING);
helper.setText(DEFAULT_TEXT);
return d;
}
}
File diff suppressed because it is too large Load Diff
@@ -71,7 +71,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
public void updateHistory() {
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
sort();
}
@@ -27,13 +27,16 @@ public class UsbStatusButton extends MenuButton {
public void setDevices(List<Device> devices) {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY)) {
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase " + (device.isNeedsPassphraseSent() ? "Off" : "On"));
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
toggleItem.setOnAction(event -> {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(event1 -> {
EventManager.get().post(new RequestOpenWalletsEvent());
if(!device.getModel().externalPassphraseEntry()) {
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
}
});
togglePassphraseService.setOnFailed(event1 -> {
AppServices.showErrorDialog("Error toggling passphrase", event1.getSource().getException().getMessage());
@@ -9,14 +9,18 @@ import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
public class UtxosChart extends BarChart<String, Number> {
private static final int MAX_BARS = 8;
private static final int MAX_BARS = 10;
private static final String OTHER_CATEGORY = "Other";
private static final int TOOLTIP_SHOW_DELAY = 50;
private List<Entry> selectedEntries;
private int totalUtxos;
@@ -57,9 +61,11 @@ public class UtxosChart extends BarChart<String, Number> {
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
utxoSeries.getData().set(i, newData);
installTooltip(newData);
}
} else {
utxoSeries.getData().add(newData);
installTooltip(newData);
}
}
@@ -74,12 +80,21 @@ 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) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
return entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
}
return ((UtxoEntry)entry).getDescription();
}
private void installTooltip(XYChart.Data<String, Number> item) {
Tooltip.uninstall(item.getNode(), null);
String satsValue = String.format(Locale.ENGLISH, "%,d", item.getYValue());
Tooltip tooltip = new Tooltip(item.getXValue() + "\n" + satsValue + " sats");
tooltip.setShowDelay(Duration.millis(TOOLTIP_SHOW_DELAY));
Tooltip.install(item.getNode(), tooltip);
}
public void select(List<Entry> entries) {
Set<Node> selectedBars = lookupAll(".chart-bar.selected");
for(Node selectedBar : selectedBars) {
@@ -99,7 +99,7 @@ public class UtxosTreeTable extends CoinTreeTable {
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
public void updateHistory() {
//Utxo entries should have already been updated, so only a resort required
if(!getRoot().getChildren().isEmpty()) {
sort();
@@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import org.controlsfx.control.textfield.CustomPasswordField;
public class ViewPasswordField extends CustomPasswordField {
public ViewPasswordField() {
super();
getStyleClass().add("view-password-text-field");
}
@Override
protected Skin<?> createDefaultSkin() {
return new ViewPasswordFieldSkin(this) {
@Override
public ObjectProperty<Node> leftProperty() {
return ViewPasswordField.this.leftProperty();
}
@Override
public ObjectProperty<Node> rightProperty() {
return ViewPasswordField.this.rightProperty();
}
};
}
}
@@ -0,0 +1,83 @@
package com.sparrowwallet.sparrow.control;
import impl.org.controlsfx.skin.CustomTextFieldSkin;
import javafx.animation.FadeTransition;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.scene.Cursor;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.controlsfx.control.textfield.CustomPasswordField;
public abstract class ViewPasswordFieldSkin extends CustomTextFieldSkin {
private static final Duration FADE_DURATION = Duration.millis(350);
public static final char BULLET = '\u25CF';
private boolean mask = true;
public ViewPasswordFieldSkin(CustomPasswordField textField) {
super(textField);
Region viewPasswordButton = new Region();
viewPasswordButton.getStyleClass().addAll("graphic");
StackPane viewPasswordButtonPane = new StackPane(viewPasswordButton);
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
viewPasswordButtonPane.setOpacity(0.0);
viewPasswordButtonPane.setCursor(Cursor.DEFAULT);
viewPasswordButtonPane.setOnMouseReleased(e -> {
if(mask) {
viewPasswordButtonPane.getStyleClass().remove("view-password-button");
viewPasswordButtonPane.getStyleClass().addAll("hide-password-button");
mask = false;
} else {
viewPasswordButtonPane.getStyleClass().remove("hide-password-button");
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
mask = true;
}
textField.setText(textField.getText());
textField.end();
});
textField.rightProperty().set(viewPasswordButtonPane);
final FadeTransition fader = new FadeTransition(FADE_DURATION, viewPasswordButtonPane);
fader.setCycleCount(1);
textField.textProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable arg0) {
String text = textField.getText();
boolean isTextEmpty = text == null || text.isEmpty();
boolean isButtonVisible = fader.getNode().getOpacity() > 0;
if (isTextEmpty && isButtonVisible) {
setButtonVisible(false);
} else if (!isTextEmpty && !isButtonVisible) {
setButtonVisible(true);
}
}
private void setButtonVisible( boolean visible ) {
fader.setFromValue(visible? 0.0: 1.0);
fader.setToValue(visible? 1.0: 0.0);
fader.play();
}
});
}
@Override
protected String maskText(String txt) {
if(getSkinnable() instanceof PasswordField && mask) {
int n = txt.length();
StringBuilder passwordBuilder = new StringBuilder(n);
for (int i = 0; i < n; i++) {
passwordBuilder.append(BULLET);
}
return passwordBuilder.toString();
} else {
return txt;
}
}
}
@@ -31,6 +31,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
});
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
@@ -40,7 +41,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(420);
scrollPane.setPrefHeight(520);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
@@ -48,7 +49,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -61,6 +62,10 @@ public class WalletImportDialog extends Dialog<Wallet> {
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
MnemonicWalletKeystoreImportPane mnemonicImportPane = new MnemonicWalletKeystoreImportPane(new Bip39());
importAccordion.getPanes().add(0, mnemonicImportPane);
scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
@@ -75,7 +80,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
});
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(500);
dialogPane.setPrefHeight(600);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
@@ -8,7 +8,6 @@ import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@@ -31,8 +30,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
this.requirement = requirement;
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.password = new ViewPasswordField();
this.passwordConfirm = new ViewPasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
this.changePassword = new CheckBox("Change password");
this.deleteBackups = new CheckBox("Delete any backups");
@@ -15,9 +15,12 @@ import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class WebcamService extends ScheduledService<Image> {
@@ -89,16 +92,20 @@ public class WebcamService extends ScheduledService<Image> {
opening.set(false);
}
BufferedImage bimg = cam.getImage();
if(bimg == null) {
BufferedImage originalImage = cam.getImage();
if(originalImage == null) {
return null;
}
Image image = SwingFXUtils.toFXImage(bimg, null);
CroppedDimension cropped = getCroppedDimension(originalImage);
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
BufferedImage framedImage = getFramedImage(originalImage, cropped);
Image image = SwingFXUtils.toFXImage(framedImage, null);
updateValue(image);
if(System.currentTimeMillis() > (lastQrSampleTime + QR_SAMPLE_PERIOD_MILLIS)) {
readQR(bimg);
readQR(originalImage, croppedImage);
lastQrSampleTime = System.currentTimeMillis();
}
@@ -125,18 +132,49 @@ public class WebcamService extends ScheduledService<Image> {
return super.cancel();
}
private void readQR(BufferedImage bufferedImage) {
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
Result result = readQR(wideImage);
if(result == null) {
result = readQR(croppedImage);
}
if(result != null) {
resultProperty.set(result);
}
}
private Result readQR(BufferedImage bufferedImage) {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = qrReader.decode(bitmap);
resultProperty.set(result);
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
} catch(ReaderException e) {
// fall thru, it means there is no QR code in image
return null;
}
}
private BufferedImage getFramedImage(BufferedImage image, CroppedDimension cropped) {
BufferedImage clone = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = (Graphics2D)clone.getGraphics();
g2d.drawImage(image, 0, 0, null);
float[] dash1 = {10.0f};
g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(resolution == WebcamResolution.HD ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10));
g2d.dispose();
return clone;
}
private CroppedDimension getCroppedDimension(BufferedImage bufferedImage) {
int dimension = Math.min(bufferedImage.getWidth(), bufferedImage.getHeight());
int squareSize = dimension / 2;
int x = (bufferedImage.getWidth() - squareSize) / 2;
int y = (bufferedImage.getHeight() - squareSize) / 2;
return new CroppedDimension(x, y, squareSize);
}
public Result getResult() {
return resultProperty.get();
}
@@ -172,4 +210,16 @@ public class WebcamService extends ScheduledService<Image> {
public BooleanProperty openingProperty() {
return opening;
}
private static class CroppedDimension {
public int x;
public int y;
public int length;
public CroppedDimension(int x, int y, int length) {
this.x = x;
this.y = y;
this.length = length;
}
}
}
@@ -1,27 +0,0 @@
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,35 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import java.util.List;
public class ChildWalletsAddedEvent extends WalletChangedEvent {
private final Storage storage;
private final List<Wallet> childWallets;
public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
super(masterWallet);
this.storage = storage;
this.childWallets = List.of(childWallet);
}
public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, List<Wallet> childWallets) {
super(masterWallet);
this.storage = storage;
this.childWallets = childWallets;
}
public Storage getStorage() {
return storage;
}
public List<Wallet> getChildWallets() {
return childWallets;
}
public String getMasterWalletId() {
return storage.getWalletId(getWallet());
}
}
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
public class FunctionActionEvent {
private final Function function;
private final Wallet wallet;
public FunctionActionEvent(Function function, Wallet wallet) {
this.function = function;
this.wallet = wallet;
}
public Function getFunction() {
return function;
}
public Wallet getWallet() {
return wallet;
}
public boolean selectFunction() {
return true;
}
}
@@ -7,7 +7,9 @@ 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.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
import java.util.ArrayList;
import java.util.List;
@@ -60,12 +62,21 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getWhirlpoolMixTransactions() {
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
return transactionEntries.stream()
.filter(txEntry -> txEntry.getValue() == 0 && txEntry.isConfirming() && !allOutputsSpent(txEntry))
.map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
private boolean allOutputsSpent(TransactionEntry txEntry) {
return txEntry.getChildren().stream()
.map(refEntry -> ((TransactionHashIndexEntry)refEntry))
.filter(txRefEntry -> txRefEntry.getType() == HashIndexEntry.Type.OUTPUT)
.allMatch(txRefEntry -> txRefEntry.getHashIndex().isSpent());
}
}
@@ -1,20 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveActionEvent {
private final Wallet wallet;
public class ReceiveActionEvent extends FunctionActionEvent {
public ReceiveActionEvent(NodeEntry receiveEntry) {
this.wallet = receiveEntry.getWallet();
super(Function.RECEIVE, receiveEntry.getWallet());
}
public ReceiveActionEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
super(Function.RECEIVE, wallet);
}
}
@@ -1,22 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.net.Aopp;
public class ReceiveProofEvent {
private final Wallet wallet;
private final Aopp aopp;
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
this.wallet = wallet;
this.aopp = aopp;
}
public Wallet getWallet() {
return wallet;
}
public Aopp getAopp() {
return aopp;
}
}
@@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
public class SelectEntryEvent {
private final Entry entry;
public SelectEntryEvent(Entry entry) {
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
public Wallet getWallet() {
return entry.getWallet();
}
}
@@ -2,23 +2,24 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import java.util.List;
public class SendActionEvent {
private final Wallet wallet;
public class SendActionEvent extends FunctionActionEvent {
private final List<BlockTransactionHashIndex> utxos;
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this.wallet = wallet;
super(Function.SEND, wallet);
this.utxos = utxos;
}
public Wallet getWallet() {
return wallet;
}
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
}
@Override
public boolean selectFunction() {
return !getUtxos().isEmpty();
}
}
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class SorobanInitiatedEvent {
private Wallet wallet;
public SorobanInitiatedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -1,17 +1,29 @@
package com.sparrowwallet.sparrow.event;
import javafx.scene.Node;
public class StatusEvent {
public static final int DEFAULT_SHOW_DURATION_SECS = 20;
private final String status;
private final Node graphic;
private final int showDuration;
public StatusEvent(String status) {
this(status, DEFAULT_SHOW_DURATION_SECS);
this(status, null, DEFAULT_SHOW_DURATION_SECS);
}
public StatusEvent(String status, Node graphic) {
this(status, graphic, DEFAULT_SHOW_DURATION_SECS);
}
public StatusEvent(String status, int showDuration) {
this(status, null, showDuration);
}
public StatusEvent(String status, Node graphic, int showDuration) {
this.status = status;
this.graphic = graphic;
this.showDuration = showDuration;
}
@@ -19,6 +31,10 @@ public class StatusEvent {
return status;
}
public Node getGraphic() {
return graphic;
}
public int getShowDuration() {
return showDuration;
}
@@ -15,4 +15,20 @@ public class WalletChangedEvent {
public Wallet getWallet() {
return wallet;
}
public boolean fromThisOrNested(Wallet targetWallet) {
if(wallet.equals(targetWallet)) {
return true;
}
return wallet.isNested() && targetWallet.getChildWallets().contains(wallet);
}
public boolean toThisOrNested(Wallet targetWallet) {
if(wallet.equals(targetWallet)) {
return true;
}
return targetWallet.isNested() && wallet.getChildWallets().contains(targetWallet);
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletDeletedEvent extends WalletChangedEvent {
public WalletDeletedEvent(Wallet wallet) {
super(wallet);
}
}
@@ -3,25 +3,37 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
import java.util.List;
import java.util.*;
/**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
*/
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
private final List<Entry> entries;
//Contains the changed entry mapped to the entry that changed it, if changed recursively (otherwise null)
private final Map<Entry, Entry> entrySourceMap;
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entries = List.of(entry);
this(wallet, List.of(entry));
}
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
super(wallet);
this.entries = entries;
this.entrySourceMap = new LinkedHashMap<>();
for(Entry entry : entries) {
entrySourceMap.put(entry, null);
}
}
public List<Entry> getEntries() {
return entries;
public WalletEntryLabelsChangedEvent(Wallet wallet, Map<Entry, Entry> entrySourceMap) {
super(wallet);
this.entrySourceMap = entrySourceMap;
}
public Collection<Entry> getEntries() {
return entrySourceMap.keySet();
}
public Entry getSource(Entry entry) {
return entrySourceMap.get(entry);
}
}
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Storage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -16,11 +16,13 @@ import java.util.stream.Collectors;
public class WalletHistoryChangedEvent extends WalletChangedEvent {
private final Storage storage;
private final List<WalletNode> historyChangedNodes;
private final List<WalletNode> nestedHistoryChangedNodes;
public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes) {
public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes, List<WalletNode> nestedHistoryChangedNodes) {
super(wallet);
this.storage = storage;
this.historyChangedNodes = historyChangedNodes;
this.nestedHistoryChangedNodes = nestedHistoryChangedNodes;
}
public String getWalletId() {
@@ -31,6 +33,17 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
return historyChangedNodes;
}
public List<WalletNode> getNestedHistoryChangedNodes() {
return nestedHistoryChangedNodes;
}
public List<WalletNode> getAllHistoryChangedNodes() {
List<WalletNode> allHistoryChangedNodes = new ArrayList<>(historyChangedNodes.size() + nestedHistoryChangedNodes.size());
allHistoryChangedNodes.addAll(historyChangedNodes);
allHistoryChangedNodes.addAll(nestedHistoryChangedNodes);
return allHistoryChangedNodes;
}
public List<WalletNode> getReceiveNodes() {
return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList());
}
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import java.util.ArrayList;
import java.util.List;
/**
@@ -19,6 +20,32 @@ public class WalletNodeHistoryChangedEvent {
}
public WalletNode getWalletNode(Wallet wallet) {
WalletNode changedNode = getNode(wallet);
if(changedNode != null) {
return changedNode;
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
changedNode = getNode(childWallet);
if(changedNode != null) {
return changedNode;
}
}
}
Wallet notificationWallet = wallet.getNotificationWallet();
if(notificationWallet != null) {
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
if(ElectrumServer.getScriptHash(notificationWallet, notificationNode).equals(scriptHash)) {
return notificationNode;
}
}
return null;
}
private WalletNode getNode(Wallet wallet) {
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
if(changedNode != null) {
@@ -30,8 +57,8 @@ public class WalletNodeHistoryChangedEvent {
}
private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
WalletNode purposeNode = wallet.getNode(keyPurpose);
for(WalletNode addressNode : purposeNode.getChildren()) {
WalletNode purposeNode = wallet.getNode(keyPurpose);
for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) {
if(ElectrumServer.getScriptHash(wallet, addressNode).equals(scriptHash)) {
return addressNode;
}
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletWatchLastChangedEvent extends WalletSettingsChangedEvent {
private final Integer watchLast;
public WalletWatchLastChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, Integer watchLast) {
super(wallet, pastWallet, walletId);
this.watchLast = watchLast;
}
public Integer getWatchLast() {
return watchLast;
}
}
@@ -37,6 +37,7 @@ public class FontAwesome5 extends GlyphFont {
EYE('\uf06e'),
FEATHER_ALT('\uf56b'),
FILE_CSV('\uf6dd'),
FILE_IMPORT('\uf56f'),
HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'),
HAND_HOLDING_WATER('\uf4c1'),
@@ -44,6 +45,7 @@ public class FontAwesome5 extends GlyphFont {
INFO_CIRCLE('\uf05a'),
KEY('\uf084'),
LAPTOP('\uf109'),
LINK('\uf0c1'),
LOCK('\uf023'),
LOCK_OPEN('\uf3c1'),
MINUS_CIRCLE('\uf056'),
@@ -56,6 +58,7 @@ public class FontAwesome5 extends GlyphFont {
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),
REPLY_ALL('\uf122'),
ROBOT('\uf544'),
SATELLITE_DISH('\uf7c0'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
@@ -70,7 +73,10 @@ public class FontAwesome5 extends GlyphFont {
TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'),
UNDO('\uf0e2'),
USER('\uf007'),
USER_FRIENDS('\uf500'),
USER_PLUS('\uf234'),
USER_SLASH('\uf506'),
WALLET('\uf555'),
WEIGHT('\uf496');
@@ -18,7 +18,7 @@ public class Bip39 implements KeystoreMnemonicImport {
@Override
public String getKeystoreImportDescription() {
return "Import or generate your 12 to 24 word mnemonic and optional passphrase.";
return "Generate or import your 12 to 24 word mnemonic and optional passphrase.";
}
@Override
@@ -50,7 +50,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
Wallet wallet = new Wallet();
wallet.setName(cf.name);
wallet.setPolicyType(PolicyType.MULTI);
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
Keystore keystore = new Keystore(extKey.name);
@@ -78,7 +78,6 @@ public class CaravanMultisig implements WalletImport, WalletExport {
return wallet;
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}
@@ -57,7 +57,6 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
return keystore;
} catch (Exception e) {
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -85,7 +85,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard.";
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > 0 feature on your Coldcard.";
}
@Override
@@ -154,7 +154,6 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
return wallet;
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
}
@@ -197,7 +196,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
for(Keystore keystore : wallet.getKeystores()) {
if(multipleDerivations) {
writer.append("# derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
writer.append("Derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
}
writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase()).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
if(multipleDerivations) {
@@ -29,7 +29,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > 0 feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
}
@Override
@@ -86,7 +86,6 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
}
} catch (Exception e) {
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
@@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
@@ -13,12 +12,12 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Random;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
@@ -42,7 +41,6 @@ public class Config {
private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true;
private boolean showLoadingLog = true;
private boolean showUtxosChart = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
@@ -52,13 +50,19 @@ public class Config {
private ServerType serverType;
private String publicElectrumServer;
private String coreServer;
private List<String> recentCoreServers;
private CoreAuthType coreAuthType;
private File coreDataDir;
private String coreAuth;
private String electrumServer;
private List<String> recentElectrumServers;
private File electrumServerCert;
private boolean useProxy;
private String proxyServer;
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
private int maxPageSize = DEFAULT_PAGE_SIZE;
private boolean usePayNym;
private boolean sameAppMixing;
private Double appWidth;
private Double appHeight;
@@ -270,15 +274,6 @@ public class Config {
flush();
}
public boolean isShowUtxosChart() {
return showUtxosChart;
}
public void setShowUtxosChart(boolean showUtxosChart) {
this.showUtxosChart = showUtxosChart;
flush();
}
public boolean isPreventSleep() {
return preventSleep;
}
@@ -384,8 +379,10 @@ public class Config {
}
public void changePublicServer() {
List<String> otherServers = Arrays.stream(PublicElectrumServer.values()).map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
List<String> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
if(!otherServers.isEmpty()) {
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
}
}
public String getCoreServer() {
@@ -397,6 +394,23 @@ public class Config {
flush();
}
public List<String> getRecentCoreServers() {
return recentCoreServers;
}
public void addRecentCoreServer(String coreServer) {
if(recentCoreServers == null) {
recentCoreServers = new ArrayList<>();
}
if(!recentCoreServers.contains(coreServer)) {
recentCoreServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(coreServer)))
.findFirst().ifPresent(existingUrl -> recentCoreServers.remove(existingUrl));
recentCoreServers.add(coreServer);
flush();
}
}
public CoreAuthType getCoreAuthType() {
return coreAuthType;
}
@@ -433,6 +447,31 @@ public class Config {
flush();
}
public List<String> getRecentElectrumServers() {
return recentElectrumServers;
}
public void addRecentServer() {
if(serverType == ServerType.BITCOIN_CORE && coreServer != null) {
addRecentCoreServer(coreServer);
} else if(serverType == ServerType.ELECTRUM_SERVER && electrumServer != null) {
addRecentElectrumServer(electrumServer);
}
}
public void addRecentElectrumServer(String electrumServer) {
if(recentElectrumServers == null) {
recentElectrumServers = new ArrayList<>();
}
if(!recentElectrumServers.contains(electrumServer)) {
recentElectrumServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(electrumServer)))
.findFirst().ifPresent(existingUrl -> recentElectrumServers.remove(existingUrl));
recentElectrumServers.add(electrumServer);
flush();
}
}
public File getElectrumServerCert() {
return electrumServerCert;
}
@@ -460,6 +499,32 @@ public class Config {
flush();
}
public int getMaxServerTimeout() {
return maxServerTimeout;
}
public int getMaxPageSize() {
return maxPageSize;
}
public boolean isUsePayNym() {
return usePayNym;
}
public void setUsePayNym(boolean usePayNym) {
this.usePayNym = usePayNym;
flush();
}
public boolean isSameAppMixing() {
return sameAppMixing;
}
public void setSameAppMixing(boolean sameAppMixing) {
this.sameAppMixing = sameAppMixing;
flush();
}
public Double getAppWidth() {
return appWidth;
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
@@ -23,7 +24,7 @@ public class Descriptor implements WalletImport, WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
String outputDescriptorString = outputDescriptor.toString(true);
outputStream.write(outputDescriptorString.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
@@ -226,7 +226,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
WalletNode purposeNode = wallet.getNode(keyPurpose);
purposeNode.fillToIndex(keyPurposes.get(keyPurpose).size() - 1);
for(WalletNode addressNode : purposeNode.getChildren()) {
if(address.equals(wallet.getAddress(addressNode))) {
if(address.equals(addressNode.getAddress())) {
addressNode.setLabel(ew.labels.get(key));
}
}
@@ -264,7 +264,6 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
return wallet;
} catch (Exception e) {
log.error("Error importing Electrum Wallet", e);
throw new ImportException("Error importing Electrum Wallet", e);
}
}
@@ -0,0 +1,46 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.File;
import java.io.InputStream;
public class GordianSeedTool implements KeystoreFileImport {
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public String getName() {
return "Gordian Seed Tool";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.GORDIAN_SEED_TOOL;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Only QR imports are supported.");
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public String getKeystoreImportDescription() {
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
}
@@ -39,16 +39,17 @@ public class Hwi {
private static boolean isPromptActive = false;
public List<Device> enumerate(String passphrase) throws ImportException {
String output = null;
try {
List<String> command;
if(passphrase != null && !passphrase.isEmpty()) {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString());
} else {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
}
isPromptActive = true;
String output = execute(command);
output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
if(devices == null) {
throw new ImportException("Error scanning, check devices are ready");
@@ -56,7 +57,19 @@ public class Hwi {
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
} catch(IOException e) {
log.error("Error executing " + HWI_VERSION_DIR, e);
throw new ImportException(e);
throw new ImportException("Error executing HWI", e);
} catch(Exception e) {
if(output != null) {
try {
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
JsonElement error = result.get("error");
throw new ImportException(error.getAsString());
} catch(Exception ex) {
log.error("Error parsing JSON: " + output, e);
throw new ImportException("Error parsing JSON: " + output, e);
}
}
throw e;
} finally {
isPromptActive = false;
}
@@ -166,9 +179,9 @@ public class Hwi {
isPromptActive = true;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath));
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
} else {
output = execute(getDeviceCommand(device, Command.SIGN_MESSAGE, message, derivationPath));
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
@@ -196,9 +209,9 @@ public class Hwi {
isPromptActive = true;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_TX, psbtBase64));
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
} else {
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
@@ -220,7 +233,7 @@ public class Hwi {
} catch(IOException e) {
throw new SignTransactionException("Could not sign PSBT", e);
} catch(PSBTParseException e) {
throw new SignTransactionException("Could not parsed signed PSBT", e);
throw new SignTransactionException("Could not parse signed PSBT", e);
} finally {
isPromptActive = false;
}
@@ -229,7 +242,31 @@ public class Hwi {
private String execute(List<String> command) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
}
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
List<String> processArguments = new ArrayList<>(arguments);
processArguments.add("--stdin");
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
Process process = processBuilder.start();
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString());
for(String commandArgument : commandArguments) {
writer.write(" \"");
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
writer.write("\"");
}
writer.flush();
}
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
}
private synchronized File getHwiExecutable(Command command) {
@@ -387,30 +424,42 @@ public class Hwi {
return result.get("success").getAsBoolean();
}
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
addChainType(elements, false);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements);
addChainType(elements, true);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
addChainType(elements);
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
addChainType(elements);
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
addChainType(elements, true);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private void addChainType(List<String> elements) {
private void addChainType(List<String> elements, boolean commandPresent) {
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--chain");
elements.add(elements.size() - 1, getChainName(Network.get()));
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.get()));
}
}
@@ -424,6 +473,15 @@ public class Hwi {
return network.toString();
}
private String escape(String passphrase) {
Platform platform = Platform.getCurrent();
if(platform == Platform.WINDOWS) {
return passphrase.replace("\"", "\\\"");
}
return passphrase;
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
@@ -36,29 +36,27 @@ public class JsonPersistence implements Persistence {
}
@Override
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage) throws IOException, StorageException {
Wallet wallet;
try(Reader reader = new FileReader(storage.getWalletFile())) {
wallet = gson.fromJson(reader, Wallet.class);
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
return new WalletAndKey(wallet, null, null, childWallets);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
return loadWallet(storage, password, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
Wallet wallet;
ECKey encryptionKey;
@@ -66,27 +64,26 @@ public class JsonPersistence implements Persistence {
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
wallet = gson.fromJson(reader, Wallet.class);
wallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(wallet));
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
return new WalletAndKey(wallet, encryptionKey, keyDeriver, childWallets);
}
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
private Map<WalletAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(File childFile : walletFiles) {
Wallet childWallet = loadWallet(childFile, encryptionKey);
childWallet.getPurposeNodes().forEach(purposeNode -> purposeNode.setWallet(childWallet));
Storage childStorage = new Storage(childFile);
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
childStorage.setKeyDeriver(getKeyDeriver());
childWallet.setMasterWallet(masterWallet);
childWallets.put(new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
childWallets.put(new WalletAndKey(childWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;
@@ -431,7 +428,7 @@ public class JsonPersistence implements Persistence {
@Override
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
if(keystore.hasPrivateKey()) {
if(keystore.hasMasterPrivateKey()) {
jsonObject.remove("extendedPublicKey");
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
}
@@ -62,10 +62,8 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
return keystore;
} catch (IllegalArgumentException e) {
log.error("Error getting " + getName() + " keystore - not an output descriptor");
throw new ImportException("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e);
} catch (Exception e) {
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -8,4 +8,7 @@ import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport, FileImport {
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
boolean isKeystoreImportScannable();
default boolean isFileFormatAvailable() {
return true;
};
}
@@ -9,9 +9,9 @@ import java.io.IOException;
import java.io.OutputStream;
public interface Persistence {
WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException;
WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
void updateWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
@@ -10,11 +10,16 @@ public class SeedSigner extends SpecterDIY {
@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.";
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SEEDSIGNER;
}
@Override
public boolean isFileFormatAvailable() {
return false;
}
}
@@ -101,24 +101,26 @@ public class Sparrow implements WalletImport, WalletExport {
java.nio.file.Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
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;
if(persistenceType != PersistenceType.JSON || !isEncrypted(tempFile)) {
File tempTypedFile = new File(tempFile.getParentFile(), tempFile.getName() + "." + persistenceType.getExtension());
tempFile.renameTo(tempTypedFile);
tempFile = tempTypedFile;
}
storage = new Storage(persistenceType, tempFile);
if(!isEncrypted(tempFile)) {
wallet = storage.loadUnencryptedWallet().getWallet();
} else {
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
wallet = walletBackupAndKey.getWallet();
wallet.decrypt(walletBackupAndKey.getKey());
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
WalletAndKey walletAndKey = storage.loadEncryptedWallet(password);
wallet = walletAndKey.getWallet();
wallet.decrypt(walletAndKey.getKey());
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.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) {
@@ -35,7 +35,6 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
return keystore;
} catch(IOException e) {
log.error("Error getting " + getName() + " keystore", e);
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@@ -93,7 +93,6 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return wallet;
}
} catch(Exception e) {
log.error("Error importing " + getName() + " wallet", e);
throw new ImportException("Error importing " + getName() + " wallet", e);
}
@@ -13,6 +13,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
@@ -36,7 +37,6 @@ public class Storage {
public static final String WALLETS_DIR = "wallets";
public static final String WALLETS_BACKUP_DIR = "backup";
public static final String CERTS_DIR = "certs";
public static final String TEMP_BACKUP_PREFIX = "tmp";
public static final List<String> RESERVED_WALLET_NAMES = List.of("temp");
private Persistence persistence;
@@ -85,14 +85,14 @@ public class Storage {
return "";
}
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this);
public WalletAndKey loadUnencryptedWallet() throws IOException, StorageException {
WalletAndKey masterWalletAndKey = persistence.loadWallet(this);
encryptionPubKey = NO_PASSWORD_KEY;
return migrateToDb(masterWalletAndKey);
}
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this, password);
public WalletAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
WalletAndKey masterWalletAndKey = persistence.loadWallet(this, password);
encryptionPubKey = ECKey.fromPublicOnly(masterWalletAndKey.getEncryptionKey());
return migrateToDb(masterWalletAndKey);
}
@@ -134,14 +134,6 @@ public class Storage {
}
}
public void backupTempWallet() {
try {
backupWallet(TEMP_BACKUP_PREFIX);
} catch(IOException e) {
log.error("Error creating " + TEMP_BACKUP_PREFIX + " backup wallet", e);
}
}
private void backupWallet(String prefix) throws IOException {
File backupDir = getWalletsBackupDir();
@@ -172,22 +164,15 @@ public class Storage {
deleteBackups(null);
}
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);
}
private boolean hasStartedSince(File lastBackup) {
try {
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(lastBackup.getName()));
ProcessHandle.Info processInfo = ProcessHandle.current().info();
return (processInfo.startInstant().isPresent() && processInfo.startInstant().get().isAfter(date.toInstant()));
} catch(Exception e) {
log.error("Error parsing date for backup file " + lastBackup.getName(), e);
return false;
}
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
}
private void deleteBackups(String prefix) {
@@ -197,11 +182,6 @@ public class Storage {
}
}
public File getTempBackup() {
File[] backups = getBackups(TEMP_BACKUP_PREFIX);
return backups.length == 0 ? null : backups[0];
}
File[] getBackups(String prefix) {
File backupDir = getWalletsBackupDir();
String walletName = persistence.getWalletName(walletFile, null);
@@ -227,7 +207,7 @@ public class Storage {
return null;
}
private WalletBackupAndKey migrateToDb(WalletBackupAndKey masterWalletAndKey) throws IOException, StorageException {
private WalletAndKey migrateToDb(WalletAndKey masterWalletAndKey) throws IOException, StorageException {
if(getType() == PersistenceType.JSON) {
log.info("Migrating " + masterWalletAndKey.getWallet().getName() + " from JSON to DB persistence");
masterWalletAndKey = migrateType(PersistenceType.DB, masterWalletAndKey.getWallet(), masterWalletAndKey.getEncryptionKey());
@@ -236,7 +216,7 @@ public class Storage {
return masterWalletAndKey;
}
private WalletBackupAndKey migrateType(PersistenceType type, Wallet wallet, ECKey encryptionKey) throws IOException, StorageException {
private WalletAndKey migrateType(PersistenceType type, Wallet wallet, ECKey encryptionKey) throws IOException, StorageException {
File existingFile = walletFile;
try {
@@ -373,7 +353,7 @@ public class Storage {
}
public static PersistenceType detectPersistenceType(File walletFile) {
try(Reader reader = new FileReader(walletFile)) {
try(Reader reader = new FileReader(walletFile, StandardCharsets.UTF_8)) {
int firstChar = reader.read();
if(firstChar == 'U' || firstChar == '{') {
@@ -525,22 +505,34 @@ public class Storage {
return ownerOnly;
}
public static class LoadWalletService extends Service<WalletBackupAndKey> {
public static class LoadWalletService extends Service<WalletAndKey> {
private final Storage storage;
private final SecureString password;
public LoadWalletService(Storage storage) {
this.storage = storage;
this.password = null;
}
public LoadWalletService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
}
@Override
protected Task<WalletBackupAndKey> createTask() {
protected Task<WalletAndKey> createTask() {
return new Task<>() {
protected WalletBackupAndKey call() throws IOException, StorageException {
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
password.clear();
return walletBackupAndKey;
protected WalletAndKey call() throws IOException, StorageException {
WalletAndKey walletAndKey;
if(password != null) {
walletAndKey = storage.loadEncryptedWallet(password);
password.clear();
} else {
walletAndKey = storage.loadUnencryptedWallet();
}
return walletAndKey;
}
};
}
@@ -5,16 +5,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.Map;
public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
public class WalletAndKey implements Comparable<WalletAndKey> {
private final Wallet wallet;
private final Wallet backupWallet;
private final ECKey encryptionKey;
private final Key key;
private final Map<WalletBackupAndKey, Storage> childWallets;
private final Map<WalletAndKey, Storage> childWallets;
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletBackupAndKey, Storage> childWallets) {
public WalletAndKey(Wallet wallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletAndKey, Storage> childWallets) {
this.wallet = wallet;
this.backupWallet = backupWallet;
this.encryptionKey = encryptionKey;
this.key = encryptionKey == null ? null : new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
this.childWallets = childWallets;
@@ -24,10 +22,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
return wallet;
}
public Wallet getBackupWallet() {
return backupWallet;
}
public ECKey getEncryptionKey() {
return encryptionKey;
}
@@ -36,7 +30,7 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
return key;
}
public Map<WalletBackupAndKey, Storage> getChildWallets() {
public Map<WalletAndKey, Storage> getChildWallets() {
return childWallets;
}
@@ -50,7 +44,7 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
}
@Override
public int compareTo(WalletBackupAndKey other) {
public int compareTo(WalletAndKey other) {
return wallet.compareTo(other.wallet);
}
}
@@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.wallet.*;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPool;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.exception.FlywayValidateException;
@@ -33,6 +34,9 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -55,23 +59,24 @@ public class DbPersistence implements Persistence {
private Wallet masterWallet;
private final Map<Wallet, DirtyPersistables> dirtyPersistablesMap = new HashMap<>();
private ExecutorService updateExecutor;
public DbPersistence() {
EventManager.get().register(this);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage) throws IOException, StorageException {
return loadWallet(storage, null, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
return loadWallet(storage, password, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
migrate(storage, MASTER_SCHEMA, encryptionKey);
@@ -82,29 +87,22 @@ public class DbPersistence implements Persistence {
return walletDao.getMainWallet(MASTER_SCHEMA);
});
File backupFile = storage.getTempBackup();
Wallet backupWallet = null;
if(backupFile != null) {
Persistence backupPersistence = PersistenceType.DB.getInstance();
backupPersistence.setKeyDeriver(keyDeriver);
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
backupPersistence.close();
}
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
createUpdateExecutor(masterWallet);
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
return new WalletAndKey(masterWallet, encryptionKey, keyDeriver, childWallets);
}
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
private Map<WalletAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, 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<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(String schema : childSchemas) {
migrate(storage, schema, encryptionKey);
@@ -116,8 +114,7 @@ public class DbPersistence implements Persistence {
childWallet.setMasterWallet(masterWallet);
return childWallet;
});
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null);
childWallets.put(new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
childWallets.put(new WalletAndKey(wallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;
@@ -157,7 +154,21 @@ public class DbPersistence implements Persistence {
@Override
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException {
updatePassword(storage, encryptionPubKey);
update(storage, wallet, getFilePassword(encryptionPubKey));
updateExecutor.execute(() -> {
try {
update(storage, wallet, getFilePassword(encryptionPubKey));
} catch(Exception e) {
log.error("Error updating wallet db", e);
}
});
}
private synchronized void createUpdateExecutor(Wallet masterWallet) {
if(updateExecutor == null) {
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(false).priority(Thread.NORM_PRIORITY).build();
updateExecutor = Executors.newSingleThreadExecutor(factory);
}
}
private File renameToDbFile(File walletFile) throws IOException {
@@ -196,11 +207,18 @@ public class DbPersistence implements Persistence {
jdbi.useHandle(handle -> {
WalletDao walletDao = handle.attach(WalletDao.class);
try {
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
return;
}
walletDao.setSchema(getSchema(wallet));
if(dirtyPersistables.clearHistory) {
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class);
detachedLabelDao.clearAndAddAll(wallet);
walletNodeDao.clearHistory(wallet);
blockTransactionDao.clear(wallet.getId());
}
@@ -234,6 +252,10 @@ public class DbPersistence implements Persistence {
BlockTransaction blkTx = wallet.getTransactions().get(txid);
blockTransactionDao.addOrUpdate(wallet, txid, blkTx);
}
if(!dirtyPersistables.clearHistory) {
DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class);
detachedLabelDao.clearAndAddAll(wallet);
}
}
if(dirtyPersistables.label != null) {
@@ -248,6 +270,10 @@ public class DbPersistence implements Persistence {
walletDao.updateGapLimit(wallet.getId(), dirtyPersistables.gapLimit);
}
if(dirtyPersistables.watchLast != null) {
walletDao.updateWatchLast(wallet.getId(), dirtyPersistables.watchLast);
}
if(!dirtyPersistables.labelEntries.isEmpty()) {
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
@@ -332,6 +358,7 @@ public class DbPersistence implements Persistence {
if(wallet.isMasterWallet()) {
masterWallet = wallet;
createUpdateExecutor(masterWallet);
}
}
@@ -544,6 +571,24 @@ public class DbPersistence implements Persistence {
@Override
public void close() {
EventManager.get().unregister(this);
if(updateExecutor != null) {
updateExecutor.shutdown();
try {
if(!updateExecutor.awaitTermination(1, TimeUnit.MINUTES)) {
updateExecutor.shutdownNow();
}
closeDataSource();
} catch (InterruptedException e) {
updateExecutor.shutdownNow();
closeDataSource();
}
} else {
closeDataSource();
}
}
private void closeDataSource() {
if(dataSource != null && !dataSource.isClosed()) {
dataSource.close();
}
@@ -631,90 +676,108 @@ public class DbPersistence implements Persistence {
return false;
}
@Subscribe
public void walletDeleted(WalletDeletedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).deleteAccount = true);
}
}
@Subscribe
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true;
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true);
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes());
if(persistsFor(event.getWallet()) && !event.getHistoryChangedNodes().isEmpty()) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()));
}
}
@Subscribe
public void walletLabelChanged(WalletLabelChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel();
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel());
}
}
@Subscribe
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight();
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight());
}
}
@Subscribe
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit();
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit());
}
}
@Subscribe
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries());
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries()));
}
}
@Subscribe
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos());
updateExecutor.execute(() -> 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;
updateExecutor.execute(() -> 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());
updateExecutor.execute(() -> {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).changedUtxoMixes.putAll(event.getChangedUtxoMixes());
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).removedUtxoMixes.putAll(event.getRemovedUtxoMixes());
});
}
}
@Subscribe
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores());
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores()));
}
}
@Subscribe
public void keystoreEncryptionChanged(KeystoreEncryptionChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).encryptionKeystores.addAll(event.getChangedKeystores());
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).encryptionKeystores.addAll(event.getChangedKeystores()));
}
}
@Subscribe
public void walletWatchLastChanged(WalletWatchLastChangedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).watchLast = event.getWatchLast());
}
}
private static class DirtyPersistables {
public boolean deleteAccount;
public boolean clearHistory;
public final List<WalletNode> historyNodes = new ArrayList<>();
public String label;
public Integer blockHeight = null;
public Integer gapLimit = null;
public Integer watchLast = null;
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean mixConfig;
@@ -725,11 +788,13 @@ public class DbPersistence implements Persistence {
public String toString() {
return "Dirty Persistables" +
"\nDelete account:" + deleteAccount +
"\nClear history:" + clearHistory +
"\nNodes:" + historyNodes +
"\nLabel:" + label +
"\nBlockHeight:" + blockHeight +
"\nGap limit:" + gapLimit +
"\nWatch last:" + watchLast +
"\nTx labels:" + labelEntries.stream().filter(entry -> entry instanceof TransactionEntry).map(entry -> ((TransactionEntry)entry).getBlockTransaction().getHash().toString()).collect(Collectors.toList()) +
"\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()) +
@@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.*;
public interface DetachedLabelDao {
@SqlQuery("select entry, label from detachedLabel")
@RegisterRowMapper(DetachedLabelMapper.class)
Map<String, String> getAll();
@SqlBatch("insert into detachedLabel (entry, label) values (?, ?)")
void insertDetachedLabels(List<String> entries, List<String> labels);
@SqlUpdate("delete from detachedLabel")
void clear();
default void clearAndAddAll(Wallet wallet) {
clear();
List<String> entries = new ArrayList<>();
List<String> labels = new ArrayList<>();
for(Map.Entry<String, String> labelEntry : new HashSet<>(wallet.getDetachedLabels().entrySet())) {
entries.add(truncate(labelEntry.getKey(), 80));
labels.add(truncate(labelEntry.getValue(), 255));
}
insertDetachedLabels(entries, labels);
}
default String truncate(String label, int length) {
return (label != null && label.length() > length ? label.substring(0, length) : label);
}
}
@@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.io.db;
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 DetachedLabelMapper implements RowMapper<Map.Entry<String, String>> {
@Override
public Map.Entry<String, String> map(ResultSet rs, StatementContext ctx) throws SQLException {
String entry = rs.getString("entry");
String label = rs.getString("label");
return new Map.Entry<>() {
@Override
public String getKey() {
return entry;
}
@Override
public String getValue() {
return label;
}
@Override
public String setValue(String value) {
return null;
}
};
}
}

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