Compare commits

...
189 Commits
Author SHA1 Message Date
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
Craig Raw 97f312cb93 v1.5.2 2021-10-29 15:47:48 +02:00
Craig Raw 269fd7f0da fix issue when displaying tx with new wallet 2021-10-29 15:43:52 +02:00
Craig Raw f0a813d031 handle concurrent modifications when saving address nodes 2021-10-29 14:39:51 +02:00
Craig Raw ceb5d85648 various minor fixes 2021-10-29 14:33:47 +02:00
Craig Raw 6b6b23b51a pay to the next fresh address of any open wallet via dropdown on send tab address field 2021-10-29 13:23:27 +02:00
Craig Raw 180e76f0f8 support discovery of singlesig connected hardware wallet accounts 2021-10-29 11:22:34 +02:00
Craig Raw d3b1c51115 increase toggle connecting animation rate 2021-10-28 16:55:24 +02:00
Craig Raw 72cb696451 add bip44 account discovery 2021-10-28 16:44:36 +02:00
Craig Raw 784fa5e1e8 minor whirlpool related fixes 2021-10-28 13:52:47 +02:00
Craig Raw 37aa3c9712 support account renaming, configuration of mix index range 2021-10-27 16:14:35 +02:00
Craig Raw 9520f6d218 upgrade to nightjar 0.2.19 (whirlpool client 0.23.37), minor tx diagram improvements 2021-10-27 12:01:04 +02:00
Craig Raw f4810bb568 show transaction diagram on every transaction headers screen 2021-10-26 17:35:58 +02:00
craigrawandGitHub febd5c33a2 Merge pull request #281 from grettke/master
Add mailmap file for Craig Raw addresses.
2021-10-25 13:47:20 +02:00
Craig Raw 4b39316821 recount mixes if mix data unavailable, correct mix status cell to remove mix progress from non-mixing utxos, show registered inputs total in tooltip 2021-10-25 12:25:18 +02:00
Grant Rettke 69544a2dc8 Add mailmap file for Craig Raw addresses. 2021-10-24 20:03:13 -05:00
craigrawandGitHub 2a0412320a Merge pull request #205 from RequestPrivacy/build
Add note to bitcoinbinary.org in build info
2021-10-22 11:49:09 +02:00
craigrawandGitHub fffd489bcf Merge pull request #278 from alaznem/master
Add other requirements for building to main README
2021-10-22 11:47:56 +02:00
craigrawandGitHub 70a6b259ba Merge pull request #267 from sashafklein/patch-1
Clarify how to run sparrow from CL
2021-10-22 11:43:50 +02:00
Craig Raw 26c177bd00 follow up 2021-10-22 11:19:26 +02:00
Craig Raw 1497b3d3bb add fee rate selection for premix 2021-10-22 11:01:58 +02:00
Alazne MoralesandGitHub 230f3c3db7 Add other requirements for building to main README
The "other requirements" are listed in the [reproducible builds readme](https://github.com/sparrowwallet/sparrow/blob/master/docs/reproducible.md#other-requirements) already. However when trying to quickly build the binaries and follow the main README instructions, the last paragraph about "[m]ore detailed instructions on reproducing the binaries are provided" can be overlooked very easily. Because the paragraph is about the specific topic "reproducible builds" and not building the binaries from source in general.
2021-10-21 20:39:50 +00:00
Craig Raw 813e0f3ab1 retrieve stored index from mixconfig on whirlpool wallet load 2021-10-21 14:38:13 +02:00
Craig Raw 237f97852d add coin selection filter to exclude immature coinbase outputs 2021-10-16 11:10:24 +02:00
Sasha KleinandGitHub 97ef580602 Clarify how to run sparrow from CL
This tiny Readme edit would, I think, have preempted the confusion that prompted #263 . I thought that the general install installed a global command, and didn't think, for some time, to run from within the directory. Dumb, but figured clarifying the doc would be better than just figuring it out silently on my own.
2021-10-14 19:35:37 -05:00
Craig Raw 2debc07375 import and export a wallet as an output descriptor in a text file 2021-10-14 14:27:13 +02:00
Craig Raw 1b3a35fda7 improve ui with whirlpool startup errors 2021-10-14 13:35:17 +02:00
Craig Raw 23fd597ca5 constrain mix from and mix to options to match those supported in the whirlpool client 2021-10-14 11:12:31 +02:00
Craig Raw eb012f638e fix popup window placement on moving active window back to primary screen 2021-10-14 10:23:04 +02:00
Craig Raw 63259a2056 fix lcd text rendering issue on osx 2021-10-14 09:55:26 +02:00
Craig Raw bad209ea5b automatically increase gap limit if required by postmix handler 2021-10-13 15:21:14 +02:00
Craig Raw 776fcb3044 permit mixing from badbank 2021-10-12 12:46:42 +02:00
Craig Raw 63ec856e87 improve transaction file opening io 2021-10-11 11:13:56 +02:00
Craig Raw ada8ca28e8 add tor proxy prompt text 2021-10-11 09:51:25 +02:00
Craig Raw ebd629db3a v1.5.1 2021-10-07 13:11:28 +02:00
Craig Raw c18a2f4388 improve tor identity management 2021-10-07 12:23:28 +02:00
Craig Raw 6f95dbe309 support bitcoin core connections over tor 2021-10-06 20:37:58 +02:00
Craig Raw 576253e651 fix remaining issue when utxo date is null 2021-10-06 10:08:56 +02:00
Craig Raw b9d6cb17d4 indicate when entered seed is of unsupported electrum type 2021-10-06 08:23:08 +02:00
Craig Raw 3b730a1711 preserve check menu item state across windows 2021-10-05 16:38:56 +02:00
Craig Raw 8c0a1932cf add prevent computer sleep functionality 2021-10-05 16:16:32 +02:00
Craig Raw 35b57f9d69 add txid to transactions csv download 2021-10-05 10:55:20 +02:00
Craig Raw d6ad7f4808 handle null dates in date cell 2021-10-05 10:39:11 +02:00
Craig Raw 3e47a49f49 core connection: default to cookie authentication when user/pass are not filled in 2021-10-05 10:08:51 +02:00
Craig Raw 4c817d243d fix long specter desktop keystore labels 2021-10-04 14:45:02 +02:00
Craig Raw 22b7b659f3 include amount in transaction pie chart tooltip 2021-10-04 14:44:19 +02:00
Craig Raw 9dd6068e69 make temp backup permanent when created in a previous process 2021-10-04 13:49:41 +02:00
craigrawandGitHub 54baee57e1 Merge pull request #235 from romanz/master
Allow running `sparrow` script from other paths
2021-10-04 12:54:48 +02:00
Roman Zeyde 581aaf288e Allow running sparrow script from other paths
IIUC, it currently assumes that it's run locally using `./sparrow`.
2021-10-04 13:44:42 +03:00
Craig Raw 2fa47e640d various whirlpool-related improvements 2021-10-04 12:16:03 +02:00
Craig Raw ea03dece72 add lock wallet functionality 2021-10-01 15:47:01 +02:00
Craig Raw 8e0b9a3ea0 ask for passphrase re-entry when creating a bip39 wallet 2021-09-30 12:49:03 +02:00
Craig Raw 67179127e3 minor whirlpool related fixes 2021-09-30 10:52:40 +02:00
Craig Raw 4ebee8a8f3 dont allow double click to receive on whirlpool wallet 2021-09-29 10:41:16 +02:00
Craig Raw 2548e77d90 followup to disable adding accounts to p2sh legacy multisig 2021-09-29 10:32:43 +02:00
Craig Raw 58e3b9dcdd add multiple account functionality 2021-09-29 10:11:51 +02:00
Craig Raw 429b733140 prevent setting separate passwords on child wallets 2021-09-27 13:24:29 +02:00
Craig Raw 56e3a54ae0 clarify and improve wallet password and keystore passphrase entry 2021-09-27 12:49:46 +02:00
Craig Raw a934ffa76c fix issues when removing selected items from utxotreetable 2021-09-27 11:46:50 +02:00
Craig Raw 86a49e0d9a fix npe showing the mix to dialog when non-standard wallets are loaded 2021-09-27 10:08:48 +02:00
Craig Raw 761e9c9b7e use wallet unit when displaying pool denomination 2021-09-27 09:33:41 +02:00
Craig Raw 712241873f show reason for mix error in tooltip 2021-09-27 08:54:42 +02:00
Craig Raw 31f9cca33a handle electrs batching detection better 2021-09-27 08:15:20 +02:00
Craig Raw 395e90e2a5 undo #181: always create bitcoin core wallet if missing 2021-09-27 08:14:35 +02:00
RequestPrivacy 90dea201b9 Add note to bitcoinbinary.org in build info
Actively encourage people to document their build process and provide their work back to the community. Further, push adoption of bitcoinbinary.org as a site for verification and establish sparrow as a well proven/verified wallet.
2021-09-22 15:38:49 +02:00
231 changed files with 9993 additions and 1525 deletions
+2
View File
@@ -0,0 +1,2 @@
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
+9 -2
View File
@@ -19,7 +19,12 @@ or for those without SSH credentials:
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
These binaries can be built from source using
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
`sudo apt install -y rpm fakeroot binutils`
The Sparrow binaries can be built from source using
`./gradlew jpackage`
@@ -29,9 +34,11 @@ When updating to the latest HEAD
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
## Running
If you prefer to run Sparrow directly from source, it can be launched with
If you prefer to run Sparrow directly from source, it can be launched from within the project directory with
`./sparrow`
+42 -18
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.0'
def sparrowVersion = '1.5.6'
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.17-SNAPSHOT')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.27')
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",
@@ -142,7 +148,7 @@ run {
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
}
@@ -191,10 +197,11 @@ 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 += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
}
addExtraDependencies("javafx")
@@ -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.17-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.17-SNAPSHOT') {
module('nightjar-0.2.27.jar', 'com.sparrowwallet.nightjar', '0.2.27') {
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')
@@ -549,7 +573,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
View File
@@ -51,7 +51,7 @@ sudo apt install -y rpm fakeroot binutils
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.0"
GIT_TAG="1.5.2"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
+1 -1
Submodule drongo updated: c9e57fad01...ee732fb223
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
cd `dirname $0`
args="$*"
args="${args%"${args##*[![:space:]]}"}"
-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.0</string>
<string>1.5.6</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.soroban.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;
@@ -35,6 +42,8 @@ import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
@@ -42,6 +51,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
@@ -52,10 +62,7 @@ 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.stage.*;
import javafx.util.Duration;
import org.controlsfx.control.Notifications;
import org.controlsfx.control.StatusBar;
@@ -67,6 +74,7 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
@@ -86,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;
@@ -118,31 +132,52 @@ public class AppController implements Initializable {
@FXML
private CheckMenuItem openWalletsInNewWindows;
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem hideEmptyUsedAddresses;
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem useHdCameraResolution;
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showLoadingLog;
@FXML
private CheckMenuItem showUtxosChart;
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showTxHex;
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
@FXML
private MenuItem minimizeToTray;
@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();
@FXML
private StackPane rootStack;
@@ -159,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<>();
@@ -203,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();
@@ -222,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) {
@@ -273,18 +317,36 @@ public class AppController implements Initializable {
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
setTheme(null);
openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows());
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
useHdCameraResolution.setSelected(Config.get().isHdCapture());
showTxHex.setSelected(Config.get().isShowTransactionHex());
showLoadingLog.setSelected(Config.get().isShowLoadingLog());
showUtxosChart.setSelected(Config.get().isShowUtxosChart());
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
showTxHexProperty.set(Config.get().isShowTransactionHex());
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
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.disableProperty().bind(findMixingPartner.disableProperty());
findMixingPartner.setDisable(true);
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
});
setServerType(Config.get().getServerType());
serverToggle.setSelected(isConnected());
@@ -360,9 +422,10 @@ public class AppController implements Initializable {
Parent root = loader.load();
AboutController controller = loader.getController();
Stage stage = new Stage();
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);
@@ -370,6 +433,9 @@ public class AppController implements Initializable {
controller.setStage(stage);
controller.initializeView();
setStageIcon(stage);
stage.setOnShowing(event -> {
AppServices.moveToActiveWindowScreen(stage, 600, 460);
});
return stage;
} catch(IOException e) {
@@ -429,10 +495,7 @@ public class AppController implements Initializable {
if(file.exists()) {
try {
byte[] bytes = new byte[(int)file.length()];
FileInputStream stream = new FileInputStream(file);
stream.read(bytes);
stream.close();
byte[] bytes = Files.readAllBytes(file.toPath());
String name = file.getName();
try {
@@ -561,6 +624,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);
}
@@ -644,6 +724,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<>();
@@ -719,12 +811,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());
@@ -738,6 +824,12 @@ public class AppController implements Initializable {
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
}
public void preventSleep(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setPreventSleep(item.isSelected());
AppServices.get().setPreventSleep(item.isSelected());
}
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
@@ -794,8 +886,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 -> {
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
openWallet(storage, walletBackupAndKey, 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();
@@ -819,9 +924,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();
}
@@ -829,8 +936,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);
@@ -879,7 +984,7 @@ public class AppController implements Initializable {
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
} else {
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullName(), copyKeystore);
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
if(optionalPassphrase.isPresent()) {
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
@@ -906,6 +1011,17 @@ 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);
} else if(Config.get().isUsePayNym() && SorobanServices.canWalletMix(wallet)) {
String walletId = storage.getWalletId(wallet);
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
soroban.setPaymentCode(copy);
}
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
}
for(int i = 0; i < wallet.getKeystores().size(); i++) {
@@ -941,6 +1057,7 @@ public class AppController implements Initializable {
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
new Electrum(),
new SpecterDesktop(),
new Descriptor(),
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig(),
new KeystoneSinglesig(), new KeystoneMultisig(),
@@ -1136,10 +1253,132 @@ public class AppController implements Initializable {
}
}
public void sweepPrivateKey(ActionEvent event) {
Wallet wallet = null;
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
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(), false);
payNymDialog.showAndWait();
}
}
public void minimizeToTray(ActionEvent event) {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
public void lockWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
EventManager.get().post(new WalletLockEvent(selectedWalletForm.getMasterWallet()));
}
}
public void 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) {
@@ -1189,7 +1428,6 @@ public class AppController implements Initializable {
tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
tab.setOnCloseRequest(event -> {
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
@@ -1202,13 +1440,17 @@ public class AppController implements Initializable {
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
subTabs.getStyleClass().add("master-only");
setSubTabsVisible(subTabs, false);
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
if(selectedTab != null) {
@@ -1232,12 +1474,11 @@ public class AppController implements Initializable {
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet, backupWallet);
Tab masterTab = subTabs.getTabs().get(0);
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
Platform.runLater(() -> {
subTabs.getStyleClass().remove("master-only");
subTabs.getStyleClass().add("wallet-subtabs");
setSubTabsVisible(subTabs, true);
});
}
}
@@ -1247,11 +1488,27 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
}
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
if(visible) {
subTabs.getStyleClass().remove("master-only");
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
subTabs.getStyleClass().add("wallet-subtabs");
}
} else {
if(!subTabs.getStyleClass().contains("master-only")) {
subTabs.getStyleClass().add("master-only");
}
subTabs.getStyleClass().remove("wallet-subtabs");
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
try {
Tab subTab = new Tab();
subTab.setClosable(false);
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
String label = wallet.getLabel() != null ? wallet.getLabel() : (wallet.isMasterWallet() ? wallet.getAutomaticName() : wallet.getName());
Label subTabLabel = new Label(label);
subTabLabel.setPadding(new Insets(0, 3, 0, 3));
subTabLabel.setGraphic(getSubTabGlyph(wallet));
subTabLabel.setContentDisplay(ContentDisplay.TOP);
subTabLabel.setAlignment(Pos.TOP_CENTER);
@@ -1269,8 +1526,16 @@ public class AppController implements Initializable {
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
subTab.setUserData(tabData);
if(!wallet.isWhirlpoolChildWallet()) {
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
}
subTabs.getTabs().add(subTab);
subTabs.getTabs().sort((o1, o2) -> {
WalletTabData tabData1 = (WalletTabData) o1.getUserData();
WalletTabData tabData2 = (WalletTabData) o2.getUserData();
return tabData1.getWallet().compareTo(tabData2.getWallet());
});
subTabs.getSelectionModel().select(subTab);
return walletForm;
@@ -1296,42 +1561,21 @@ public class AppController implements Initializable {
return tabGlyph;
}
private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex();
return account < 0 ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
}
public WalletForm getSelectedWalletForm() {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane)selectedTab.getContent();
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
return subWalletTabData.getWalletForm();
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;
}
public void openExamples(ActionEvent event) {
try {
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
addTransactionTab("p2sh", null, "0100000003a5ee1a0fd80dfbc3142df136ab56e082b799c13aa977c048bdf8f61bd158652c000000006b48304502203b0160de302cded63589a88214fe499a25aa1d86a2ea09129945cd632476a12c022100c77727daf0718307e184d55df620510cf96d4b5814ae3258519c0482c1ca82fa0121024f4102c1f1cf662bf99f2b034eb03edd4e6c96793cb9445ff519aab580649120ffffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0000000006a47304402204089adb8a1de1a9e22aa43b94d54f1e54dc9bea745d57df1a633e03dd9ede3c2022037d1e53e911ed7212186028f2e085f70524930e22eb6184af090ba4ab779a5b90121030644cb394bf381dbec91680bdf1be1986ad93cfb35603697353199fb285a119effffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0010000009300493046022100a07b2821f96658c938fa9c68950af0e69f3b2ce5f8258b3a6ad254d4bc73e11e022100e82fab8df3f7e7a28e91b3609f91e8ebf663af3a4dc2fd2abd954301a5da67e701475121022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e2103a767c7221e9f15f870f1ad9311f5ab937d79fcaeee15bb2c722bca515581b4c052aeffffffff02a3b81b00000000001976a914ea00917f128f569cbdf79da5efcd9001671ab52c88ac80969800000000001976a9143dec0ead289be1afa8da127a7dbdd425a05e25f688ac00000000");
addTransactionTab("p2sh-p2wpkh", null, "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000");
addTransactionTab("p2sh-p2wsh", null, "01000000000101708256c5896fb3f00ef37601f8e30c5b460dbcd1fca1cd7199f9b56fc4ecd5400000000023220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4cffffffff01603edb0300000000160014bbef244bcad13cffb68b5cef3017c7423675552204004730440220010d2854b86b90b7c33661ca25f9d9f15c24b88c5c4992630f77ff004b998fb802204106fc3ec8481fa98e07b7e78809ac91b6ccaf60bf4d3f729c5a75899bb664a501473044022046d66321c6766abcb1366a793f9bfd0e11e0b080354f18188588961ea76c5ad002207262381a0661d66f5c39825202524c45f29d500c6476176cd910b1691176858701695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
addTransactionTab("p2wpkh", null, "01000000000101109d2e41430bfdec7e6dfb02bf78b5827eeb717ef25210ff3203b0db8c76c9260000000000ffffffff01a032eb0500000000160014bbef244bcad13cffb68b5cef3017c742367555220247304402202f7cac3494e521018ae0be4ca18517639ef7c00658d42a9f938b2b344c8454e2022039a54218832fad5d14b331329d9042c51ee6be287e95e49ee5b96fda1f5ce13f0121026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf300000000");
addTransactionTab("p2wsh", null, "0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
//addTransactionTab("test1", null, "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900");
//addTransactionTab("3of3-1s.psbt", null, "70736274ff0100550200000001294c4871c059bb76be81e94b78059ee2e0c9b1b47f38edb6b4e75916062394930000000000feffffff01f82a0000000000001976a914e65b294f890792f2c2725d488567018d660f0cf488ac701c09004f0102aa7ed3044b1635bb800000021bf4bfc48934b7966b39bdebb689525d9b8bfed5c8b16e8c58f9afe4641d6d5f03800b5dbec0355c9f0b5e8227bc903e9d0ff1fe6ced0dcfb6d416541c7412c4331406b57041300000800000008000000080020000804f0102aa7ed3042cd31dee80000002d544b2364010378f8c6cec85f6b7ed83a8203dcdbedb97e2625f431f897b837e0363428de8fcfbfe373c0d9e1e0cc8163d886764bafe71c5822eaa232981356589145f63394f300000800000008000000080020000804f0102aa7ed3049ec7d9f580000002793e04aff18b4e40ebc48bcdc6232c54c69cf7265a38fbd85b35705e34d2d42f03368e79aa2b2b7f736d156905a7a45891df07baa2d0b7f127a537908cb82deed514130a48af300000800000008000000080020000800001012b983a000000000000220020f64748dad1cbad107761aaed5c59f25aba006498d260b440e0a091691350c9aa010569532102f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3262103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a221037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f753ae220602f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3261c130a48af300000800000008000000080020000800000000000000000220603171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a21c5f63394f300000800000008000000080020000800000000000000000220203171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a24830450221008d27cc4b03bc543726e73b69e7980e7364d6f33f979a5cd9b92fb3d050666bd002204fc81fc9c67baf7c3b77041ed316714a9c117a5bdbb020e8c771ea3bdc342434012206037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f71c06b570413000008000000080000000800200008000000000000000000000");
//addTransactionTab("signer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
//addTransactionTab("combiner.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
addTransactionTab("finalizer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
} catch(Exception e) {
log.error("Error opening examples", e);
}
}
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
if(Utils.isBase64(string) && !Utils.isHex(string)) {
addTransactionTab(name, file, Base64.getDecoder().decode(string));
@@ -1480,6 +1724,18 @@ public class AppController implements Initializable {
private ContextMenu getTabContextMenu(Tab tab) {
ContextMenu contextMenu = new ContextMenu();
if(tab.getUserData() instanceof WalletTabData walletTabData) {
MenuItem lock = new MenuItem("Lock");
Glyph lockGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lockGlyph.setFontSize(12);
lock.setGraphic(lockGlyph);
lock.disableProperty().bind(walletTabData.getWalletForm().lockedProperty());
lock.setOnAction(event -> {
EventManager.get().post(new WalletLockEvent(walletTabData.getWallet()));
});
contextMenu.getItems().addAll(lock);
}
MenuItem close = new MenuItem("Close");
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
@@ -1501,6 +1757,41 @@ public class AppController implements Initializable {
return contextMenu;
}
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
rename.setOnAction(event -> {
Label subTabLabel = (Label)subTab.getGraphic();
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(subTabLabel.getText());
Optional<String> optLabel = walletLabelDialog.showAndWait();
if(optLabel.isPresent()) {
String label = optLabel.get();
subTabLabel.setText(label);
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;
}
public void setServerType(ServerType serverType) {
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
serverToggle.getStyleClass().add("public-server");
@@ -1529,21 +1820,17 @@ public class AppController implements Initializable {
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), 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);
}
}
@@ -1559,13 +1846,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);
}
}
@@ -1595,8 +1878,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);
}
@@ -1665,19 +1948,21 @@ public class AppController implements Initializable {
} else {
saveTransaction.setVisible(false);
}
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
showTxHex.setDisable(false);
findMixingPartner.setDisable(true);
} else if(event instanceof WalletTabSelectedEvent) {
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
showTxHex.setDisable(true);
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
}
}
}
@@ -1701,7 +1986,8 @@ 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());
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
}
}
}
@@ -1709,8 +1995,22 @@ public class AppController implements Initializable {
@Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) {
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
TabData tabData = (TabData)subTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWallet().equals(event.getWallet()) && walletTabData.getWalletForm().lockedProperty().get()) {
return;
}
}
}
}
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
List<BlockTransaction> whirlpoolTransactions = event.getUnspentConfirmingWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
if(!whirlpoolTransactions.isEmpty()) {
@@ -1817,8 +2117,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);
@@ -1939,7 +2246,7 @@ public class AppController implements Initializable {
@Subscribe
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
if(event.getWalletNode() == null && event.getWallet().getTransactions().isEmpty()) {
if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) {
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(-1);
@@ -2149,12 +2456,7 @@ public class AppController implements Initializable {
}
@Subscribe
public void sendAction(SendActionEvent event) {
selectTab(event.getWallet());
}
@Subscribe
public void recieveAction(ReceiveActionEvent event) {
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
}
@@ -2167,4 +2469,22 @@ public class AppController implements Initializable {
addWalletTab(storage, event.getChildWallet(), null);
}
@Subscribe
public void walletLock(WalletLockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
exportWallet.setDisable(true);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
exportWallet.setDisable(!event.getWallet().isValid());
}
}
}
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.BlockHeader;
@@ -10,13 +9,16 @@ 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.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
@@ -24,6 +26,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -48,6 +51,7 @@ import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenURIHandler;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.*;
@@ -72,10 +76,16 @@ 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 final MainApp application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
@@ -94,13 +104,15 @@ public class AppServices {
private TorService torService;
private ScheduledService<Void> preventSleepService;
private static Integer currentBlockHeight;
private static BlockHeader latestBlockHeader;
private static Map<Integer, Double> targetBlockFeeRates;
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
@@ -148,6 +160,7 @@ public class AppServices {
this.application = application;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
}
public void start() {
@@ -156,6 +169,7 @@ public class AppServices {
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
@@ -183,6 +197,10 @@ public class AppServices {
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
restartService(versionCheckService);
}
if(config.isPreventSleep()) {
restartService(preventSleepService);
}
}
private void restartService(ScheduledService<?> service) {
@@ -382,6 +400,39 @@ public class AppServices {
return torService;
}
private ScheduledService<Void> createPreventSleepService() {
ScheduledService<Void> preventSleepService = new ScheduledService<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() {
try {
Robot robot = new Robot();
robot.keyRelease(KeyEvent.VK_F16);
} catch(Exception e) {
log.debug("Error preventing sleep", e);
}
return null;
}
};
}
};
preventSleepService.setPeriod(Duration.minutes(1));
return preventSleepService;
}
public void setPreventSleep(boolean preventSleep) {
if(preventSleepService != null) {
if(preventSleep) {
restartService(preventSleepService);
} else {
preventSleepService.cancel();
}
}
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
@@ -430,6 +481,10 @@ public class AppServices {
return get().whirlpoolServices;
}
public static SorobanServices getSorobanServices() {
return get().sorobanServices;
}
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
@@ -441,7 +496,7 @@ 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")));
@@ -494,6 +549,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);
@@ -528,11 +595,16 @@ 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 Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
public static TreeMap<Date, Set<MempoolRateSize>> getMempoolHistogram() {
return mempoolHistogram;
}
@@ -543,6 +615,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() {
@@ -581,7 +656,7 @@ public class AppServices {
}
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons);
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
@@ -654,7 +729,9 @@ public class AppServices {
public static void moveToWindowScreen(Window currentWindow, Window newWindow, double newWindowWidth, double newWindowHeight) {
Screen currentScreen = Screen.getScreens().stream().filter(screen -> screen.getVisualBounds().contains(currentWindow.getX(), currentWindow.getY())).findFirst().orElse(null);
if(currentScreen != null && !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()) && !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
if(currentScreen != null
&& ((!Double.isNaN(newWindow.getX()) && !Double.isNaN(newWindow.getY())) || !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()))
&& !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
double x = currentWindow.getX() + (currentWindow.getWidth() / 2) - (newWindowWidth / 2);
double y = currentWindow.getY() + (currentWindow.getHeight() / 2.2) - (newWindowHeight / 2);
newWindow.setX(x);
@@ -717,8 +794,6 @@ public class AppServices {
Platform.runLater(() -> {
if("bitcoin".equals(uri.getScheme())) {
openBitcoinUri(uri);
} else if("aopp".equals(uri.getScheme())) {
openAddressOwnershipProof(uri);
}
});
}
@@ -753,20 +828,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());
@@ -803,6 +864,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.0";
public static final String APP_VERSION = "1.5.6";
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";
@@ -70,7 +69,6 @@ public class MainApp extends Application {
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
Config.get().setCoreWallet(Bwt.DEFAULT_CORE_WALLET);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
@@ -78,16 +76,14 @@ 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());
}
}
}
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE && Config.get().getCoreWallet() == null) {
Config.get().setCoreMultiWallet(Boolean.TRUE);
Config.get().setCoreWallet("");
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
@@ -2,11 +2,16 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,111 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
private final ComboBox<StandardAccount> standardAccountCombo;
private boolean discoverAccounts = false;
public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
setTitle("Add Account");
dialogPane.setHeaderText("Choose an account to add:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
key.setFontSize(50);
dialogPane.setGraphic(key);
final VBox content = new VBox(10);
content.setPrefHeight(50);
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
List<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
}
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
}
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
discoverButton.setOnAction(event -> {
discoverAccounts = true;
});
}
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() {
@Override
public String toString(StandardAccount account) {
if(account == null) {
return "None Available";
}
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
return "Whirlpool Accounts";
}
return account.getName();
}
@Override
public StandardAccount fromString(String string) {
return null;
}
});
if(standardAccountCombo.getItems().isEmpty()) {
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
okButton.setDisable(true);
} else {
standardAccountCombo.getSelectionModel().select(0);
}
content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null));
}
public boolean isDiscoverAccounts() {
return discoverAccounts;
}
}
@@ -48,7 +48,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
}
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -68,21 +68,23 @@ public class AddressTreeTable extends CoinTreeTable {
}
}
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
}
}
}
}
}
});
});
}
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
@@ -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);
@@ -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();
@@ -0,0 +1,71 @@
package com.sparrowwallet.sparrow.control;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
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");
setupComboButtonField(super.rightProperty());
disabledProperty().addListener((observable, oldValue, newValue) -> {
if(comboProperty.isNotNull().get()) {
comboProperty.get().setVisible(!newValue);
}
});
}
private void setupComboButtonField(ObjectProperty<Node> rightProperty) {
Region showComboButton = new Region();
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
StackPane showComboButtonPane = new StackPane(showComboButton);
showComboButtonPane.getStyleClass().addAll("combo-button"); //$NON-NLS-1$
showComboButtonPane.setCursor(Cursor.DEFAULT);
showComboButtonPane.setOnMouseReleased(e -> {
if(comboProperty.isNotNull().get()) {
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;
}
}
});
rightProperty.set(showComboButtonPane);
}
public ComboBox<?> getComboProperty() {
return comboProperty.get();
}
public ObjectProperty<ComboBox<?>> walletComboProperty() {
return comboProperty;
}
public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(comboProperty);
}
}
@@ -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();
}
}
@@ -35,10 +35,14 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
} else {
setContextMenu(null);
} else if(utxoEntry.getHashIndex().getDate() != null) {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
} else {
setText("Unknown");
setContextMenu(null);
}
Tooltip tooltip = new Tooltip();
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
}
public static String getShortDateFormat(Date date) {
if(date == null) {
return "Unknown";
}
Date now = new Date();
long elapsed = (now.getTime() - date.getTime()) / 1000;
@@ -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);
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
private final Wallet masterWallet;
private final List<StandardAccount> availableAccounts;
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
super(operationFingerprints);
this.masterWallet = masterWallet;
this.availableAccounts = availableAccounts;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
}
@Subscribe
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
setResult(event.getDiscoveredKeystores());
}
}
@@ -10,12 +10,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
@@ -33,8 +35,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane {
@@ -46,29 +47,32 @@ public class DevicePane extends TitledDescriptionPane {
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final List<StandardAccount> availableAccounts;
private final Device device;
private CustomPasswordField pinField;
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private SplitMenuButton importButton;
private ButtonBase importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.keyDerivation = requiredDerivation;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -91,6 +95,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -113,6 +118,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -135,6 +141,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -149,6 +156,29 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
this.availableAccounts = availableAccounts;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createDiscoverKeystoresButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
}
private void initialise(Device device) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
@@ -199,35 +229,39 @@ public class DevicePane extends TitledDescriptionPane {
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
});
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
}
}
importButton.managedProperty().bind(importButton.visibleProperty());
@@ -277,6 +311,17 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void createDiscoverKeystoresButton() {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
discoverKeystoresButton.setVisible(false);
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@@ -287,7 +332,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);
@@ -327,7 +372,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 -> {
@@ -430,7 +475,9 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(true);
} else {
showOperationButton();
setContent(getTogglePassphraseOn());
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
setContent(getTogglePassphraseOn());
}
}
} else {
setError("Incorrect PIN", null);
@@ -578,7 +625,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...");
@@ -614,6 +661,63 @@ public class DevicePane extends TitledDescriptionPane {
signMessageService.start();
}
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
accountDerivationPaths.put(availableAccount, derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
walletDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
});
walletDiscoveryService.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();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
setDescription("Discovering...");
showHideLink.setVisible(false);
getXpubsService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
@@ -622,7 +726,8 @@ public class DevicePane extends TitledDescriptionPane {
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
@@ -635,6 +740,10 @@ public class DevicePane extends TitledDescriptionPane {
signMessageButton.setDefaultButton(defaultDevice);
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@@ -642,6 +751,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@@ -651,7 +761,8 @@ public class DevicePane extends TitledDescriptionPane {
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import");
Button importDerivationButton = new Button("Import Custom Derivation");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -660,7 +771,8 @@ public class DevicePane extends TitledDescriptionPane {
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
@@ -679,6 +791,6 @@ public class DevicePane extends TitledDescriptionPane {
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
}
}
@@ -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;
@@ -69,10 +70,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
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,11 +114,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@@ -145,6 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
setTooltip(tooltip);
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
@@ -326,6 +336,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);
@@ -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);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
@@ -12,11 +13,13 @@ import java.io.*;
public class FileKeystoreImportPane extends FileImportPane {
protected final Wallet wallet;
private final KeystoreFileImport importer;
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
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;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
@@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
EventManager.get().post(new KeystoreImportEvent(keystore));
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
}
@@ -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);
@@ -7,6 +7,7 @@ import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
@@ -20,11 +21,15 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
this(walletName, keystore, false);
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = new ViewPasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
@@ -40,6 +45,13 @@ public class KeystorePassphraseDialog extends Dialog<String> {
content.setPrefHeight(50);
content.getChildren().add(passphrase);
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warnGlyph.getStyleClass().add("warn-icon");
warnGlyph.setFontSize(12);
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
warnLabel.setGraphicTextGap(5);
content.getChildren().add(warnLabel);
dialogPane.setContent(content);
Platform.runLater(passphrase::requestFocus);
@@ -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()) {
@@ -103,6 +103,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 +121,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:");
@@ -10,11 +10,16 @@ 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.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
@@ -40,20 +45,15 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setContextMenu(null);
}
if(mixStatus.getMixProgress() != null) {
Tooltip tooltip = new Tooltip();
tooltip.setText("Pool: " + mixStatus.getMixProgress().getPoolId().replace("btc", " BTC"));
setTooltip(tooltip);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason());
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
@@ -67,27 +67,72 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason) {
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) {
//Old error, don't set again.
return;
}
Glyph failGlyph = getFailGlyph();
setGraphic(failGlyph);
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage());
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
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);
});
timeline.play();
} else {
setGraphic(null);
setTooltip(null);
}
}
private void setMixProgress(MixProgress mixProgress) {
private String getPlatformSleepConfig() {
Platform platform = Platform.getCurrent();
if(platform == Platform.OSX) {
return "OSX System Preferences";
} else if(platform == Platform.WINDOWS) {
return "Windows Control Panel";
}
return "system power settings";
}
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
tt.setText(status);
setTooltip(tt);
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
tt.setOnShowing(event -> {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
registeredInputsService.setOnSucceeded(eventStateHandler -> {
if(registeredInputsService.getValue() != null) {
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
}
});
registeredInputsService.start();
});
}
} else {
setGraphic(null);
setTooltip(null);
}
}
@@ -38,6 +38,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
@@ -261,7 +262,13 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
invalidLabel.setText("Unsupported Electrum seed");
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
} else {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
}
@@ -384,6 +391,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
@@ -394,7 +409,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
@@ -0,0 +1,130 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.wallet.bip47.rpc.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);
}
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,62 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.soroban.PayNym;
import com.sparrowwallet.sparrow.soroban.PayNymController;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
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);
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());
});
}
setText(null);
setGraphic(pane);
}
}
}
@@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.wallet.bip47.rpc.PaymentCode;
public class PaymentCodeTextField extends CopyableTextField {
private String paymentCodeStr;
public void setPaymentCode(PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
setText(abbrevPcode);
}
@Override
protected String getCopyText() {
return paymentCodeStr;
}
}
@@ -0,0 +1,343 @@
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 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(new ArrayList<>(AppServices.get().getOpenWallets().keySet())));
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.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
}
});
keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) {
toAddress.setText(wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).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,233 @@
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);
results = new CoinTreeTable();
results.setShowRoot(false);
results.setPrefWidth(walletForms.size() > 1 ? 950 : 850);
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
results.setPlaceholder(new Label("No results"));
if(walletForms.size() > 1) {
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());
});
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);
}
}
}
}
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);
}
}
}
@@ -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
@@ -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) {
@@ -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;
}
}
}
@@ -42,9 +42,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -48,13 +48,13 @@ 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 SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -95,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
List<Device> devices = enumerateService.getValue();
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
for(Device device : devices) {
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1);
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null);
importAccordion.getPanes().add(0, devicePane);
}
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
@@ -0,0 +1,65 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class WalletLabelDialog extends Dialog<String> {
private final CustomTextField label;
public WalletLabelDialog(String initialName) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Account Name");
dialogPane.setHeaderText("Enter a name for this account:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
final VBox content = new VBox(20);
label = (CustomTextField) TextFields.createClearableTextField();
label.setText(initialName);
label.setTextFormatter(new TextFormatter<>((change) -> {
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
return change;
}));
content.getChildren().add(label);
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(label, Validator.combine(
Validator.createEmptyValidator("Account name is required")
));
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
okButton.disableProperty().bind(isInvalid);
label.setPromptText("Account Name");
Platform.runLater(label::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : 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");
@@ -131,10 +130,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
LOAD("Enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
UPDATE_SET("Re-enter the wallet password:", "Verify Password"),
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
private final String description;
@@ -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;
}
}
}
@@ -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;
}
}
@@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import java.util.Map;
public class KeystoresDiscoveredEvent {
private final Map<StandardAccount, Keystore> discoveredKeystores;
public KeystoresDiscoveredEvent(Map<StandardAccount, Keystore> discoveredKeystores) {
this.discoveredKeystores = discoveredKeystores;
}
public Map<StandardAccount, Keystore> getDiscoveredKeystores() {
return discoveredKeystores;
}
}
@@ -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;
}
}
@@ -11,6 +11,7 @@ public class SpendUtxoEvent {
private final Wallet wallet;
private final List<BlockTransactionHashIndex> utxos;
private final List<Payment> payments;
private final List<byte[]> opReturns;
private final Long fee;
private final boolean includeSpentMempoolOutputs;
private final Pool pool;
@@ -23,15 +24,17 @@ public class SpendUtxoEvent {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.opReturns = null;
this.fee = fee;
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
this.pool = null;
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.opReturns = opReturns;
this.fee = fee;
this.includeSpentMempoolOutputs = false;
this.pool = pool;
@@ -49,6 +52,10 @@ public class SpendUtxoEvent {
return payments;
}
public List<byte[]> getOpReturns() {
return opReturns;
}
public Long getFee() {
return fee;
}
@@ -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);
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
/**
* This event is posted if the wallet's gap limit has changed, and triggers a history fetch for the new nodes.
*
*/
public class WalletGapLimitChangedEvent extends WalletChangedEvent {
public WalletGapLimitChangedEvent(Wallet wallet) {
super(wallet);
}
public int getGapLimit() {
return getWallet().getGapLimit();
}
}
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
private final WalletNode walletNode;
import java.util.Set;
public WalletHistoryStartedEvent(Wallet wallet, WalletNode walletNode) {
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
private final Set<WalletNode> walletNodes;
public WalletHistoryStartedEvent(Wallet wallet, Set<WalletNode> walletNodes) {
super(wallet, true);
this.walletNode = walletNode;
this.walletNodes = walletNodes;
}
public WalletNode getWalletNode() {
return walletNode;
public Set<WalletNode> getWalletNodes() {
return walletNodes;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLabelChangedEvent extends WalletChangedEvent {
public WalletLabelChangedEvent(Wallet wallet) {
super(wallet);
}
public String getLabel() {
return getWallet().getLabel();
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLockEvent {
private final Wallet wallet;
public WalletLockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletUnlockEvent {
private final Wallet wallet;
public WalletUnlockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,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;
}
}
@@ -12,6 +12,7 @@ public class WhirlpoolMixEvent {
private final MixProgress mixProgress;
private final Utxo nextUtxo;
private final MixFailReason mixFailReason;
private final String mixError;
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
this.wallet = wallet;
@@ -19,6 +20,7 @@ public class WhirlpoolMixEvent {
this.mixProgress = mixProgress;
this.nextUtxo = null;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
@@ -27,14 +29,16 @@ public class WhirlpoolMixEvent {
this.mixProgress = null;
this.nextUtxo = nextUtxo;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason) {
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = null;
this.mixFailReason = mixFailReason;
this.mixError = mixError;
}
public Wallet getWallet() {
@@ -56,4 +60,8 @@ public class WhirlpoolMixEvent {
public MixFailReason getMixFailReason() {
return mixFailReason;
}
public String getMixError() {
return mixError;
}
}
@@ -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'),
@@ -56,12 +57,14 @@ public class FontAwesome5 extends GlyphFont {
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),
REPLY_ALL('\uf122'),
ROBOT('\uf544'),
SATELLITE_DISH('\uf7c0'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
SIGN_OUT_ALT('\uf2f5'),
SQUARE('\uf0c8'),
SNOWFLAKE('\uf2dc'),
SORT_NUMERIC_DOWN('\uf162'),
SUN('\uf185'),
THEATER_MASKS('\uf630'),
TIMES_CIRCLE('\uf057'),
@@ -69,7 +72,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');
@@ -1,10 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import java.util.List;
@@ -31,6 +28,15 @@ public class Bip39 implements KeystoreMnemonicImport {
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
return Keystore.fromSeed(seed, derivation);
} catch (Exception e) {
try {
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);
throw new ImportException(new MnemonicException.MnemonicTypeException(DeterministicSeed.Type.ELECTRUM));
} catch(Exception ex) {
if(ex instanceof ImportException && ex.getCause() instanceof MnemonicException.MnemonicTypeException) {
throw (ImportException)ex;
}
}
throw new ImportException(e);
}
}
@@ -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);
}
}
@@ -42,23 +42,27 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COLDCARD);
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
try {
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
} catch(NullPointerException e) {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
@@ -81,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
@@ -150,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);
}
}
@@ -193,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);
}
@@ -12,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);
@@ -40,8 +40,8 @@ public class Config {
private boolean openWalletsInNewWindows = false;
private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true;
private boolean showLoadingLog = false;
private boolean showUtxosChart = true;
private boolean showLoadingLog = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private File hwi;
@@ -50,15 +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 Boolean coreMultiWallet;
private String coreWallet;
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,13 +274,12 @@ public class Config {
flush();
}
public boolean isShowUtxosChart() {
return showUtxosChart;
public boolean isPreventSleep() {
return preventSleep;
}
public void setShowUtxosChart(boolean showUtxosChart) {
this.showUtxosChart = showUtxosChart;
flush();
public void setPreventSleep(boolean preventSleep) {
this.preventSleep = preventSleep;
}
public List<File> getRecentWalletFiles() {
@@ -376,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() {
@@ -389,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;
}
@@ -416,24 +438,6 @@ public class Config {
flush();
}
public Boolean getCoreMultiWallet() {
return coreMultiWallet;
}
public void setCoreMultiWallet(Boolean coreMultiWallet) {
this.coreMultiWallet = coreMultiWallet;
flush();
}
public String getCoreWallet() {
return coreWallet;
}
public void setCoreWallet(String coreWallet) {
this.coreWallet = coreWallet;
flush();
}
public String getElectrumServer() {
return electrumServer;
}
@@ -443,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;
}
@@ -470,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;
}
@@ -0,0 +1,81 @@
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;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
public class Descriptor implements WalletImport, WalletExport {
@Override
public String getName() {
return "Output Descriptor";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.BITCOIN_CORE;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
String outputDescriptorString = outputDescriptor.toString(true);
outputStream.write(outputDescriptorString.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch(Exception e) {
throw new ExportException("Error writing output descriptor", e);
}
}
@Override
public String getWalletExportDescription() {
return "The output descriptor is a standardized description of the wallet compatible with Bitcoin Core, and can be used to create a watch-only copy using the Edit button on the Settings tab of a new Sparrow wallet.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "txt";
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public String getWalletImportDescription() {
return "Import a file containing the output descriptor of a wallet. The output descriptor is a standardized description of the wallet compatible with Bitcoin Core.";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
try {
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor.trim());
return descriptor.toWallet();
} catch(Exception e) {
throw new ImportException("Error importing output descriptor", e);
}
}
@Override
public boolean isWalletImportScannable() {
return false;
}
}
@@ -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;
}
}
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
@@ -38,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");
@@ -55,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;
}
@@ -91,6 +105,15 @@ public class Hwi {
}
}
public Map<StandardAccount, String> getXpubs(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) throws ImportException {
Map<StandardAccount, String> accountXpubs = new LinkedHashMap<>();
for(Map.Entry<StandardAccount, String> entry : accountDerivationPaths.entrySet()) {
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
}
return accountXpubs;
}
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
@@ -156,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();
@@ -186,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();
@@ -210,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;
}
@@ -219,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) {
@@ -377,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()));
}
}
@@ -414,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;
@@ -580,6 +648,28 @@ public class Hwi {
}
}
public static class GetXpubsService extends Service<Map<StandardAccount, String>> {
private final Device device;
private final String passphrase;
private final Map<StandardAccount, String> accountDerivationPaths;
public GetXpubsService(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) {
this.device = device;
this.passphrase = passphrase;
this.accountDerivationPaths = accountDerivationPaths;
}
@Override
protected Task<Map<StandardAccount, String>> createTask() {
return new Task<>() {
protected Map<StandardAccount, String> call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getXpubs(device, passphrase, accountDerivationPaths);
}
};
}
}
public static class SignPSBTService extends Service<PSBT> {
private final Device device;
private final String passphrase;
@@ -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;
};
}
@@ -10,7 +10,7 @@ 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
@@ -101,9 +101,12 @@ 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();
@@ -118,7 +121,6 @@ public class Sparrow implements WalletImport, WalletExport {
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);
}
}
@@ -62,11 +62,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
wallet.setName(specterWallet.label);
if(specterWallet.devices != null && specterWallet.devices.size() == wallet.getKeystores().size()) {
boolean uniqueLabels = specterWallet.devices.stream().map(d -> d.label).distinct().count() == specterWallet.devices.size();
boolean uniqueLabels = specterWallet.devices.stream().map(SpecterWalletDevice::getLabel).distinct().count() == specterWallet.devices.size();
for(int i = 0; i < specterWallet.devices.size(); i++) {
SpecterWalletDevice device = specterWallet.devices.get(i);
Keystore keystore = wallet.getKeystores().get(i);
keystore.setLabel(device.label + (uniqueLabels ? "" : " " + i));
keystore.setLabel(device.getLabel() + (uniqueLabels ? "" : " " + i));
WalletModel walletModel = device.getWalletModel();
if(walletModel != null) {
@@ -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);
}
@@ -161,5 +160,13 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return null;
}
public String getLabel() {
if(label == null) {
label = "Keystore";
}
return label.length() > Keystore.MAX_LABEL_LENGTH - 3 ? label.substring(0, Keystore.MAX_LABEL_LENGTH - 3) : label;
}
}
}
@@ -13,12 +13,14 @@ 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;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
@@ -62,6 +64,10 @@ public class Storage {
}
public boolean isEncrypted() throws IOException {
if(!walletFile.exists()) {
return false;
}
return persistence.isEncrypted(walletFile);
}
@@ -168,10 +174,27 @@ public class Storage {
deleteBackups(null);
}
public void deleteTempBackups() {
public void deleteTempBackups(boolean forceSave) {
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
if(backups.length > 0 && (forceSave || hasStartedSince(backups[0]))) {
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
backups[0].renameTo(permanent);
}
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
}
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;
}
}
private void deleteBackups(String prefix) {
File[] backups = getBackups(prefix);
for(File backup : backups) {
@@ -355,7 +378,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 == '{') {
@@ -511,6 +534,11 @@ public class Storage {
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;
@@ -520,8 +548,15 @@ public class Storage {
protected Task<WalletBackupAndKey> createTask() {
return new Task<>() {
protected WalletBackupAndKey call() throws IOException, StorageException {
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
password.clear();
WalletBackupAndKey walletBackupAndKey;
if(password != null) {
walletBackupAndKey = storage.loadEncryptedWallet(password);
password.clear();
} else {
walletBackupAndKey = storage.loadUnencryptedWallet();
}
return walletBackupAndKey;
}
};
@@ -531,10 +566,18 @@ public class Storage {
public static class KeyDerivationService extends Service<ECKey> {
private final Storage storage;
private final SecureString password;
private final boolean verifyPassword;
public KeyDerivationService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
this.verifyPassword = false;
}
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
this.storage = storage;
this.password = password;
this.verifyPassword = verifyPassword;
}
@Override
@@ -542,7 +585,12 @@ public class Storage {
return new Task<>() {
protected ECKey call() throws IOException, StorageException {
try {
return storage.getEncryptionKey(password);
ECKey encryptionFullKey = storage.getEncryptionKey(password);
if(verifyPassword && !ECKey.fromPublicOnly(encryptionFullKey).equals(storage.getEncryptionPubKey())) {
throw new InvalidPasswordException("Derived pubkey does not match stored pubkey");
}
return encryptionFullKey;
} finally {
password.clear();
}
@@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
@Override
public int compareTo(WalletBackupAndKey other) {
if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) {
return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal();
}
return wallet.getAccountIndex() - other.wallet.getAccountIndex();
return wallet.compareTo(other.wallet);
}
}
@@ -10,6 +10,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public interface BlockTransactionDao {
@@ -35,7 +36,8 @@ public interface BlockTransactionDao {
void clear(long wallet);
default void addBlockTransactions(Wallet wallet) {
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) {
Map<Sha256Hash, BlockTransaction> walletTransactions = new HashMap<>(wallet.getTransactions());
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : walletTransactions.entrySet()) {
blkTxEntry.getValue().setId(null);
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
}
@@ -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,6 +59,7 @@ 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);
@@ -88,11 +93,14 @@ public class DbPersistence implements Persistence {
Persistence backupPersistence = PersistenceType.DB.getInstance();
backupPersistence.setKeyDeriver(keyDeriver);
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
backupPersistence.close();
}
Map<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);
}
@@ -156,7 +164,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 {
@@ -195,6 +217,11 @@ 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) {
@@ -235,10 +262,22 @@ public class DbPersistence implements Persistence {
}
}
if(dirtyPersistables.label != null) {
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > 255 ? dirtyPersistables.label.substring(0, 255) : dirtyPersistables.label);
}
if(dirtyPersistables.blockHeight != null) {
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight);
}
if(dirtyPersistables.gapLimit != null) {
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);
@@ -323,6 +362,7 @@ public class DbPersistence implements Persistence {
if(wallet.isMasterWallet()) {
masterWallet = wallet;
createUpdateExecutor(masterWallet);
}
}
@@ -535,6 +575,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();
}
@@ -622,74 +680,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());
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()));
}
}
@Subscribe
public void walletLabelChanged(WalletLabelChangedEvent event) {
if(persistsFor(event.getWallet())) {
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())) {
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;
@@ -700,9 +792,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()) +
@@ -8,16 +8,16 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
public interface MixConfigDao {
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
@SqlQuery("select id, scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
@RegisterRowMapper(MixConfigMapper.class)
MixConfig getForWalletId(Long id);
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
long insertMixConfig(String scode, Boolean mixOnStartup, String indexRange, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, indexRange = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String indexRange, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
default void addMixConfig(Wallet wallet) {
if(wallet.getMixConfig() != null) {
@@ -32,10 +32,10 @@ public interface MixConfigDao {
}
if(mixConfig.getId() == null) {
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
mixConfig.setId(id);
} else {
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
}
}
}

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