Compare commits

...
286 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
Craig Raw b2657cdcfb v1.5.0 final 2021-09-23 15:52:33 +02:00
Craig Raw 6bbae204a6 restrict mixing to mainnet and testnet 2021-09-23 15:30:13 +02:00
Craig Raw 0b55dd8a1e ui fixes to mix start and stop 2021-09-23 14:18:37 +02:00
Craig Raw f74287697c use master wallet passphrase for child wallets when loading 2021-09-23 12:49:06 +02:00
Craig Raw 7a3e1dfa1f change default log level to warn 2021-09-23 12:28:47 +02:00
Craig Raw 427a6925ee mainnet mixing, improve pools selection, other fixes 2021-09-23 10:59:40 +02:00
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
Craig Raw c55b19af0f tor status windows visual fixes 2021-09-21 11:55:23 +02:00
Craig Raw e12f7a634a add tor status indicator to status bar 2021-09-21 11:22:09 +02:00
Craig Raw ca1f934138 update README for https cloning 2021-09-20 16:42:55 +02:00
Craig Raw 4f9b87b74e fix recursive clone via https 2021-09-20 16:37:03 +02:00
Craig Raw e1e5df78c6 fix text truncation in whirlpool dialog on windows 2021-09-20 15:51:57 +02:00
Craig Raw cfd06a8513 use cached tx0previews, only save mixconfig on apply 2021-09-20 15:30:13 +02:00
craigrawandGitHub 1c1099217b Merge pull request #198 from zeroleak/whirlpool-client-0.23.30-early5
update to whirlpool-client 0.23.31
2021-09-20 13:10:47 +02:00
zeroleak 6939d8a06a upgrade to whirlpool-client 0.23.33 2021-09-18 11:41:52 +02:00
zeroleak dbebade3ab update to whirlpool-client 0.23.31 2021-09-11 08:17:15 +02:00
Craig Raw 5895837b60 hide receive tab on whirlpool wallets 2021-09-10 15:35:40 +02:00
Craig Raw 0f9e9d9c35 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-09-10 13:58:47 +02:00
Craig Raw 59452e64ea add additional required packages 2021-09-10 13:58:18 +02:00
craigrawandGitHub 573d5b2c18 Merge pull request #195 from RequestPrivacy/patch-1
Fix skdman link
2021-09-10 10:01:47 +02:00
Craig Raw 2f0dc5bea8 ensure submodule is also on correct tag 2021-09-10 09:41:30 +02:00
RequestPrivacyandGitHub 27f7d5ad38 Fix skdman link 2021-09-09 16:16:54 +00:00
Craig Raw 414ffc2648 add first draft of reproducible build documentation 2021-09-09 14:07:45 +02:00
Craig Raw aaca9ffa16 dont allow receiving on badbank wallet, introduce skipInstaller flag in build 2021-09-08 13:22:21 +02:00
Craig Raw 44b9cee825 fix outdated reference to Java 14 and link to Java 16 JDK 2021-09-07 19:27:22 +02:00
Craig Raw c024c351ac minor fixes 2021-09-07 11:51:14 +02:00
Craig Raw ed2914f2aa replace illegal filename characters in wallet name with underscores 2021-09-06 14:49:53 +02:00
Craig Raw 0b4785e01c v1.5.0 beta1 2021-09-06 11:53:34 +02:00
Craig Raw 0c50c9cb9c make jackson jars open and non-synthetic modules 2021-09-06 10:18:36 +02:00
Craig Raw 916c2b6122 fix module issues in binary 2021-09-05 15:47:15 +02:00
Craig Raw 308a89c958 update required java to 16 in build instructions 2021-09-03 20:51:56 +02:00
Craig Raw 88ebef97d4 support mixing from all single sig wallets, handle tor proxy change, and other minor fixes 2021-09-03 17:16:37 +02:00
Craig Raw a42761981c support mixing to multisig wallets 2021-09-02 17:14:01 +02:00
Craig Raw b6f047d382 minor fixes 2021-09-02 15:26:16 +02:00
Craig Raw 8f63d89be8 avoid creating a core wallet if no wallets are open 2021-09-02 15:22:53 +02:00
Craig Raw 6e6111b47c support using stored mix indexes to avoid resending a utxo to the coordinator 2021-09-02 14:12:59 +02:00
Craig Raw e8af7c70bd refactor to WhirlpoolServices 2021-09-02 12:37:07 +02:00
Craig Raw 2fc551e35b add mix to functionality 2021-09-02 11:39:56 +02:00
Craig Raw adb77771aa add mix config persistence and initial usage 2021-09-01 13:10:46 +02:00
Craig Raw 13e01451b7 update nightjar for tor identity change functionality 2021-08-31 16:43:56 +02:00
Craig Raw aa10bcfe1a fixes for encrypted whirlpool wallets and other issues 2021-08-31 16:19:24 +02:00
craigrawandGitHub f30c00ba8f Merge pull request #183 from zeroleak/whirlpool-client-0.23.30-early4
Upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3
2021-08-31 10:57:49 +02:00
zeroleak 4577a64ad5 apply Craig's feedback
- use V2__Whirlpool.sql
- allow pools list & tx0 preview without seed
- filter DataSource wallet for refresh
2021-08-29 10:49:25 +02:00
Craig Raw 7371ca2994 add option to optimize transactions for privacy and display privacy analysis 2021-08-27 16:00:17 +02:00
zeroleak 050c4fc31e upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-27 09:20:41 +02:00
zeroleak 772370808c Merge remote-tracking branch 'remotes/origin/master' into whirlpool-client-0.23.30-early4
# Conflicts:
#	src/main/java/com/sparrowwallet/sparrow/AppServices.java
#	src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java
2021-08-24 12:18:54 +02:00
Craig Raw b22e891b7d make select all, copy on transaction hex area copy untruncated transaction hex 2021-08-24 10:52:56 +02:00
Craig Raw 57b3214c54 improvements to whirlpool dialog 2021-08-24 10:40:27 +02:00
Craig Raw 615b78b497 fix mix event handling for multiple wallets 2021-08-24 09:29:54 +02:00
Craig Raw d1ab1db1c5 fix issues with subtabs in new wallets 2021-08-23 17:27:30 +02:00
Craig Raw f5ac6a3b73 support whirlpool data storage in wallet file, add mixing ui 2021-08-23 16:36:02 +02:00
zeroleak fec45356a2 upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-22 11:26:11 +02:00
Craig Raw 37c4ff4dd7 reduce max number multisig keystores 2021-08-16 17:09:21 +02:00
Craig Raw b0877d94bf make all wallet addresses non-editable once child wallets are added 2021-08-16 13:33:05 +02:00
Craig Raw a3e4342d7d save app width and height across restarts 2021-08-16 11:25:18 +02:00
Craig Raw 90355a653f followup 2021-08-16 11:04:46 +02:00
Craig Raw 6339c3a7d7 allow multisig wallets with many (up to 50) keystores 2021-08-16 10:46:34 +02:00
Craig Raw 8f7f0d4c61 truncate labels over 255 chars before persisting to db 2021-08-16 10:15:14 +02:00
Craig Raw 74b4f51640 add menu items to copy psbt to clipboard in hex or base64 2021-08-16 08:48:14 +02:00
Craig Raw 2caee79df4 initial whirlpool integration 2021-08-12 17:50:13 +02:00
Craig Raw 34b4c39ccd add logging when event wallet id is not present 2021-08-04 10:45:01 +02:00
Craig Raw 34d1571669 fix aopp verification regression on button type 2021-08-04 10:03:18 +02:00
Craig Raw 22465b1b65 followup 2021-08-02 16:10:47 +02:00
Craig Raw b91e8eab51 fix tab loading animation for subtabbed wallets 2021-07-30 15:13:58 +02:00
Craig Raw 55e69bf263 fix naming when using a subtab wallet, import and export wallets with child wallets with db persistence on sparrow exporter 2021-07-30 14:17:21 +02:00
Craig Raw 02d3817cb1 introduce wallet subtabs 2021-07-30 09:46:42 +02:00
Craig Raw fc9cdaabb4 export UTXOs to CSV 2021-07-29 12:09:54 +02:00
Craig Raw 2aa3d83402 fix using the correct address label for the utxo label when recieving a batched tx to several labelled address 2021-07-29 11:40:15 +02:00
Craig Raw fc5d6ada36 add caching for verbose transaction lookups to avoid repeat server requests 2021-07-29 11:09:49 +02:00
Craig Raw be599fb003 fix hummingbird module definition 2021-07-21 15:30:46 +02:00
Craig Raw d79c4a13f6 upgrade to hummingbird v1.6.2 2021-07-20 11:47:56 +02:00
Craig Raw e6fce14fde handle unknown derivation path for unchained caravan key 2021-07-19 11:56:02 +02:00
Craig Raw 5c3a00b71b v1.4.3 2021-07-15 12:02:45 +02:00
Craig Raw ced4d4d337 add caravan import/export, minor ui fixes 2021-07-15 11:09:39 +02:00
Craig Raw 4a3ad9f4ff restrict shown sighash to available values 2021-07-14 15:13:38 +02:00
Craig Raw f5a72105ac minor fixes after refactoring 2021-07-12 08:56:37 +02:00
Craig Raw 0502eec0cd dont show message sign context menu item when message signing cant be performed 2021-07-09 13:35:45 +02:00
Craig Raw 8e6933b5ca increase animation rate and fragment length for UR QRs 2021-07-09 08:23:40 +02:00
Craig Raw 1fd1dec6cf rename tx segwit version field to segwit flag 2021-07-08 11:46:21 +02:00
Craig Raw 422713ff53 fix camera selection issues on linux 2021-07-05 12:58:26 +02:00
Craig Raw ada45ee75b allow selection of webcam from QR scan dialog 2021-07-05 12:33:31 +02:00
Craig Raw 2f153686dd avoid hang on closing webcam due to rescans for new camera devices 2021-07-04 19:18:02 +02:00
Craig Raw f691f1691e add missing requires directive for bwt lib 2021-07-02 13:41:22 +02:00
Craig Raw 1f9e37b40c enable max button for selected utxos without address and label filled 2021-07-02 12:05:38 +02:00
Craig Raw a1d2de1859 use all addressable script types (not just single hash types) 2021-07-02 10:44:24 +02:00
Craig Raw 143472bdfc fix save of address labels on a new wallet 2021-07-01 09:41:56 +02:00
Craig Raw b9e64d42ff accept pasting a whitespace delimited sequence of words into
a textfield of mnemonic word entry
2021-06-30 11:55:58 +02:00
Craig Raw 9a09bb8cda always delete hwi dir on osx before copying 2021-06-30 11:51:31 +02:00
Craig Raw 4b028af123 handle multiple selection when freezing and unfreezing utxos 2021-06-30 09:18:14 +02:00
Craig Raw 8033e5fd88 improve amount error labels adding dust threshold label for too low amounts 2021-06-29 12:48:02 +02:00
Craig Raw badf8c8f2f optimize and increase sampling rate of qr reading 2021-06-29 10:53:22 +02:00
Craig Raw b6a353815c switch mempool size chart to kvB when max Y value is less than 1 MvB 2021-06-28 14:14:22 +02:00
Craig Raw ea2f858dc9 close message sign dialog on platform button and escape key 2021-06-28 13:32:24 +02:00
Craig Raw 324540009a fix detection of input type on finalizing psbt wallet 2021-06-28 13:08:44 +02:00
Craig Raw 1c1f90344f update github build to java 16 2021-06-25 15:18:51 +02:00
Craig Raw 094dd45547 upgrade to gradle 7.1, java 16, javafx 16 2021-06-25 14:56:22 +02:00
Craig Raw 6d434722cc fix prev commit 2021-06-24 12:41:32 +02:00
Craig Raw c8a4ed0c3d add documentation link to bitcoin core error message 2021-06-24 10:07:54 +02:00
291 changed files with 16281 additions and 1785 deletions
+4 -9
View File
@@ -12,16 +12,11 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 15.0.2
if: ${{ runner.os == 'Linux' }}
uses: joschi/setup-jdk@v2
- name: Set up JDK 16.0.1
uses: actions/setup-java@v2
with:
java-version: 15
- name: Set up JDK 14.0.2
if: ${{ runner.os == 'Windows' }}
uses: actions/setup-java@v1
with:
java-version: 14.0.2
distribution: 'adopt'
java-version: '16.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:sparrowwallet/drongo.git
url = ../../sparrowwallet/drongo.git
+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>
+19 -5
View File
@@ -8,11 +8,23 @@ More information (and release binaries) can be found at https://sparrowwallet.co
## Building
To clone this project, use
To clone this project, use
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
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`
@@ -20,15 +32,17 @@ When updating to the latest HEAD
`git pull --recurse-submodules`
All jar files created are reproducible builds.
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
> 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`
Java 14 or higher must be installed.
Java 16 or higher must be installed.
## Configuration
+401 -29
View File
@@ -1,11 +1,11 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.kordamp.gradle.jdeps' version '0.9.0'
id 'org.beryx.jlink' version '2.22.0'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.4.2'
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 = "15"
version = "17"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -38,34 +38,45 @@ java {
}
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo')) {
exclude group: 'org.hamcrest'
exclude group: 'junit'
}
implementation('com.google.guava:guava: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')
implementation('com.github.arteam:simple-json-rpc-client:1.0')
implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('com.github.arteam:simple-json-rpc-core:1.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
}
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.0')
implementation('com.sparrowwallet:hummingbird:1.6.4')
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
}
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@@ -76,15 +87,23 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.7')
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.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')
}
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.MainApp'
}
compileJava {
options.with {
@@ -103,8 +122,13 @@ processResources {
}
}
test {
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
}
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",
@@ -120,10 +144,11 @@ run {
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
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"]
}
}
@@ -135,14 +160,17 @@ jlink {
requires 'java.xml'
requires 'java.logging'
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
@@ -154,20 +182,26 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow"]
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
"--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")
@@ -175,16 +209,16 @@ jlink {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
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) {
@@ -216,3 +250,341 @@ task packageTarDistribution(type: Tar) {
include "Sparrow/**"
}
}
extraJavaModuleInfo {
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
exports('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.core.async')
exports('com.fasterxml.jackson.core.base')
exports('com.fasterxml.jackson.core.exc')
exports('com.fasterxml.jackson.core.filter')
exports('com.fasterxml.jackson.core.format')
exports('com.fasterxml.jackson.core.io')
exports('com.fasterxml.jackson.core.json')
exports('com.fasterxml.jackson.core.json.async')
exports('com.fasterxml.jackson.core.sym')
exports('com.fasterxml.jackson.core.type')
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
requires('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.annotation')
}
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
requires('java.desktop')
requires('java.logging')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('java.sql')
requires('java.xml')
exports('com.fasterxml.jackson.databind')
exports('com.fasterxml.jackson.databind.annotation')
exports('com.fasterxml.jackson.databind.cfg')
exports('com.fasterxml.jackson.databind.deser')
exports('com.fasterxml.jackson.databind.deser.impl')
exports('com.fasterxml.jackson.databind.deser.std')
exports('com.fasterxml.jackson.databind.exc')
exports('com.fasterxml.jackson.databind.ext')
exports('com.fasterxml.jackson.databind.introspect')
exports('com.fasterxml.jackson.databind.json')
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
exports('com.fasterxml.jackson.databind.jsonschema')
exports('com.fasterxml.jackson.databind.jsontype')
exports('com.fasterxml.jackson.databind.jsontype.impl')
exports('com.fasterxml.jackson.databind.module')
exports('com.fasterxml.jackson.databind.node')
exports('com.fasterxml.jackson.databind.ser')
exports('com.fasterxml.jackson.databind.ser.impl')
exports('com.fasterxml.jackson.databind.ser.std')
exports('com.fasterxml.jackson.databind.type')
exports('com.fasterxml.jackson.databind.util')
uses('com.fasterxml.jackson.databind.Module')
}
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
exports('com.csvreader')
}
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
exports('org.slf4j.bridge')
requires('java.logging')
requires('org.slf4j')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
}
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
exports('mu')
requires('kotlin.stdlib')
requires('org.slf4j')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
exports('com.google.common.eventbus')
exports('com.google.common.net')
exports('com.google.common.base')
exports('com.google.common.collect')
exports('com.google.common.io')
exports('com.google.common.primitives')
exports('com.google.common.math')
requires('failureaccess')
requires('java.logging')
}
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
exports('org.reactfx.util')
requires('javafx.base')
requires('javafx.graphics')
requires('javafx.controls')
}
module('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')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
}
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.27.jar', 'com.sparrowwallet.nightjar', '0.2.27') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
requires('org.bouncycastle.provider')
requires('com.fasterxml.jackson.databind')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('org.json')
requires('io.reactivex.rxjava2')
exports('com.samourai.http.client')
exports('com.samourai.tor.client')
exports('com.samourai.wallet.api.backend')
exports('com.samourai.wallet.api.backend.beans')
exports('com.samourai.wallet.client.indexHandler')
exports('com.samourai.wallet.hd')
exports('com.samourai.wallet.util')
exports('com.samourai.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')
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
exports('com.samourai.whirlpool.client.whirlpool')
exports('com.samourai.whirlpool.client.whirlpool.beans')
exports('com.samourai.whirlpool.client.wallet.data.pool')
exports('com.samourai.whirlpool.client.wallet.data.utxo')
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
exports('com.samourai.whirlpool.client.wallet.data.supplier')
exports('com.samourai.whirlpool.client.mix.handler')
exports('com.samourai.whirlpool.client.mix.listener')
exports('com.samourai.whirlpool.protocol.beans')
exports('com.samourai.whirlpool.protocol.rest')
exports('com.samourai.whirlpool.client.tx0')
exports('com.samourai.wallet.segwit.bech32')
exports('com.samourai.whirlpool.client.wallet.data.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState')
exports('com.sparrowwallet.nightjar.http')
exports('com.sparrowwallet.nightjar.stomp')
exports('com.sparrowwallet.nightjar.tor')
}
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
exports('com.zeroleak.throwingsupplier')
}
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
exports('com.squareup.okhttp')
}
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
exports('com.auth0.jwt')
}
module('json-20180130.jar', 'org.json', '1.0') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
exports('com.google.protobuf')
}
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
exports('org.apache.commons.text')
}
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
requires('kotlin.stdlib')
requires('commons.compress')
requires('org.tukaani.xz')
requires('java.management')
requires('io.github.microutils.kotlin.logging')
}
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
exports('net.freehaven.tor.control')
}
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
exports('org.apache.commons.compress')
requires('org.tukaani.xz')
}
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
exports('org.tukaani.xz')
}
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
exports('com.runjva.sourceforge.jsocks.protocol')
requires('org.slf4j')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.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') {
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
}
}
@@ -0,0 +1,54 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}
@@ -0,0 +1,52 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}
@@ -0,0 +1,174 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
if(manifest == null) {
manifest = new Manifest();
}
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
}
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
for (String usesName : moduleInfo.getUses()) {
moduleVisitor.visitUse(usesName.replace('.', '/'));
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
@@ -0,0 +1,62 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
private List<String> uses = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public void uses(String uses) {
this.uses.add(uses);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
protected List<String> getUses() {
return uses;
}
}
+11
View File
@@ -0,0 +1,11 @@
## Sparrow Wallet Repository Docs
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
The documentation here is mainly developer-related resources.
### [Reproducible builds](reproducible.md)
Documentation to create and verify a build of the project against the released binaries.
+78
View File
@@ -0,0 +1,78 @@
## Reproducible builds
Reproducibility is a goal of the Sparrow Wallet project.
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
Work on resolving both of these issues is ongoing.
### Reproducing a release
#### Install Java
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
```shell
sudo apt-get install -y wget apt-transport-https gnupg
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
rm adoptopenjdk-keyring.gpg
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
sudo apt update -y
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
```
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 16.0.1.hs-adpt
```
#### Other requirements
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
```shell
sudo apt install -y rpm fakeroot binutils
```
#### Building the binaries
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.2"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
Thereafter, building should be straightforward:
```shell
cd sparrow
./gradlew jpackage
```
The binaries (and installers) will be placed in the `build/jpackage` folder.
#### Verifying the binaries are identical
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
Then compare all of the folders and files recursively:
```shell
diff -r build/jpackage/Sparrow /tmp/Sparrow
```
This command should have no output indicating that the two folders (and all their contents) are identical.
+1 -1
Submodule drongo updated: c021300797...ee732fb223
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+4 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
cd `dirname $0`
args="$*"
args="${args%"${args##*[![:space:]]}"}"
-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.4.2</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"/>
File diff suppressed because it is too large Load Diff
@@ -4,23 +4,29 @@ import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
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;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -45,6 +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.*;
@@ -69,8 +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<>();
@@ -89,11 +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;
@@ -112,6 +131,11 @@ public class AppServices {
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
if(online) {
if(Config.get().requiresInternalTor() && !isTorRunning()) {
if(torService.getState() == Worker.State.SCHEDULED) {
torService.cancel();
torService.reset();
}
torService.start();
} else {
restartServices();
@@ -135,6 +159,8 @@ public class AppServices {
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
}
public void start() {
@@ -143,6 +169,7 @@ public class AppServices {
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
@@ -170,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) {
@@ -369,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;
}
@@ -413,6 +477,14 @@ public class AppServices {
return INSTANCE;
}
public static WhirlpoolServices getWhirlpoolServices() {
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"));
@@ -424,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")));
@@ -473,6 +545,22 @@ public class AppServices {
return openWallets;
}
public Wallet getWallet(String walletId) {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public 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);
@@ -503,11 +591,20 @@ public class AppServices {
return currentBlockHeight;
}
public static BlockHeader getLatestBlockHeader() {
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;
}
@@ -518,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() {
@@ -556,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) {
@@ -629,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);
@@ -692,8 +794,6 @@ public class AppServices {
Platform.runLater(() -> {
if("bitcoin".equals(uri.getScheme())) {
openBitcoinUri(uri);
} else if("aopp".equals(uri.getScheme())) {
openAddressOwnershipProof(uri);
}
});
}
@@ -728,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());
@@ -773,9 +859,12 @@ public class AppServices {
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer();
}
@Subscribe
@@ -786,6 +875,8 @@ public class AppServices {
@Subscribe
public void newBlock(NewBlockEvent event) {
currentBlockHeight = event.getHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
latestBlockHeader = event.getBlockHeader();
String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status));
}
@@ -848,7 +939,7 @@ public class AppServices {
Platform.runLater(() -> {
if(!Window.getWindows().isEmpty()) {
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).filter(File::exists).collect(Collectors.toList());
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
}
});
@@ -921,7 +1012,7 @@ public class AppServices {
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
onlineProperty.set(false);
log.info("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
@@ -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.4.2";
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";
@@ -42,7 +41,13 @@ public class MainApp extends Application {
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
} else {
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
}
});
super.init();
}
@@ -64,7 +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);
@@ -72,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) {
@@ -90,6 +92,11 @@ public class MainApp extends Application {
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
mainStage.setWidth(Config.get().getAppWidth());
mainStage.setHeight(Config.get().getAppHeight());
}
AppController appController = AppServices.newAppWindow(stage);
if(createNewWallet) {
@@ -119,6 +126,8 @@ public class MainApp extends Application {
@Override
public void stop() throws Exception {
AppServices.get().stop();
Config.get().setAppWidth(mainStage.getWidth());
Config.get().setAppHeight(mainStage.getHeight());
mainStage.close();
if(sparrowInstance != null) {
sparrowInstance.freeLock();
@@ -2,11 +2,16 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,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;
}
}
@@ -11,55 +11,44 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, Entry> {
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("address-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
super.updateItem(addressStatus, empty);
EntryCell.applyRowStyles(this, entry);
getStyleClass().add("address-cell");
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
EntryCell.applyRowStyles(this, utxoEntry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
if(utxoEntry.isDuplicateAddress()) {
if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
setGraphic(getDuplicateGlyph());
Tooltip tt = new Tooltip();
tt.setText(getTooltipText(utxoEntry));
setTooltip(tt);
} else {
setGraphic(null);
}
});
}
}
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -34,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
addressCol.setSortable(false);
getColumns().add(addressCol);
if(address != null) {
if(address != null && !rootEntry.getWallet().isWhirlpoolChildWallet()) {
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
}
@@ -68,21 +68,23 @@ public class AddressTreeTable extends CoinTreeTable {
}
}
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
}
}
}
}
}
});
});
}
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
@@ -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();
}
}
@@ -34,11 +34,15 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
} else {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
setContextMenu(null);
} else if(utxoEntry.getHashIndex().getDate() != null) {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
} else {
setText("Unknown");
setContextMenu(null);
}
Tooltip tooltip = new Tooltip();
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
}
public static String getShortDateFormat(Date date) {
if(date == null) {
return "Unknown";
}
Date now = new Date();
long elapsed = (now.getTime() - date.getTime()) / 1000;
@@ -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);
@@ -546,7 +593,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.SINGLE_HASH_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
@@ -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;
}
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
@@ -11,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;
@@ -68,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 -> {
@@ -105,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 -> {
@@ -118,8 +128,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
actionBox.getChildren().add(receiveButton);
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
(nodeEntry.getWallet().getKeystores().get(0).hasPrivateKey() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
if(canSignMessage(nodeEntry.getWallet())) {
Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> {
@@ -127,10 +136,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
messageSignDialog.showAndWait();
});
actionBox.getChildren().add(signMessageButton);
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
}
setGraphic(actionBox);
if(nodeEntry.getWallet().isWhirlpoolChildWallet()) {
setText(address.toString().substring(0, 20) + "...");
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
@@ -140,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 -> {
@@ -268,6 +283,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(Wallet wallet) {
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
return transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
@@ -292,14 +312,52 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
}
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() != Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(null);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() == Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(null));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private 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() {
@@ -449,7 +507,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().add(receiveToAddress);
if(nodeEntry != null) {
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
@@ -514,7 +572,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
freezeUtxo.setGraphic(getFreezeGlyph());
freezeUtxo.setOnAction(AE -> {
hide();
freezeUtxo(hashIndexEntry);
freezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(freezeUtxo);
} else {
@@ -522,7 +580,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
unfreezeUtxo.setOnAction(AE -> {
hide();
unfreezeUtxo(hashIndexEntry);
unfreezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(unfreezeUtxo);
}
@@ -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));
}
}
}
@@ -80,9 +80,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(wallet);
fileChooser.setInitialFileName(wallet.getName() +
(exporter instanceof Sparrow ? "" : "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "")) +
(extension == null || extension.isEmpty() ? "" : "." + extension));
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
if(exporter instanceof Sparrow) {
fileName = wallet.getMasterName();
}
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
@@ -94,7 +96,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file) {
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
Wallet copy = wallet.copy();
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
@@ -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);
@@ -2,13 +2,13 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
@@ -19,6 +19,7 @@ public class HelpLabel extends Label {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
getStyleClass().add("help-label");
@@ -26,10 +27,10 @@ public class HelpLabel extends Label {
}
private static Glyph getHelpGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
lockGlyph.getStyleClass().add("help-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
glyph.getStyleClass().add("help-icon");
glyph.setFontSize(11);
return glyph;
}
public final StringProperty helpTextProperty() {
@@ -49,4 +50,18 @@ public class HelpLabel extends Label {
public final String getHelpText() {
return helpText == null ? "" : helpText.getValue();
}
public ObjectProperty<Node> helpGraphicProperty() {
if(helpGraphicProperty == null) {
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
}
return helpGraphicProperty;
}
private ObjectProperty<Node> helpGraphicProperty;
public final void setHelpGraphic(Node graphic) {
helpGraphicProperty().setValue(graphic);
}
}
@@ -7,6 +7,7 @@ import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
@@ -20,11 +21,15 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
this.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;
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
public static final int MAX_PERIOD_HOURS = 2;
private static final double Y_VALUE_BREAK_MVB = 3.0;
private Tooltip tooltip;
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
categoryAxis.setTickLabelRotation(0);
NumberAxis numberAxis = (NumberAxis)getYAxis();
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
return (vSizeBytes / (1000 * 1000)) + " MvB";
}
@Override
public Number fromString(String string) {
return null;
}
});
this.setOnMouseMoved(mouseEvent -> {
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
@@ -91,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()) {
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
previousFeeRate = feeRate;
}
final double maxMvB = getMaxMvB(getData());
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
if(maxMvB > Y_VALUE_BREAK_MVB) {
return (vSizeBytes / (1000 * 1000)) + " MvB";
} else {
return (vSizeBytes / (1000)) + " kvB";
}
}
@Override
public Number fromString(String string) {
return null;
}
});
if(categories.keySet().iterator().hasNext()) {
String time = categories.values().iterator().next();
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
return categories;
}
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
double maxTotal = 0d;
for(Series<String, Number> series : seriesList) {
maxTotal = Math.max(maxTotal, getMaxMvB(series));
}
return maxTotal;
}
private static double getMaxMvB(Series<String, Number> series) {
double total = 0d;
for(XYChart.Data<String, Number> data : series.getData()) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
total += mvb;
}
return total;
}
private static class ChartTooltip extends VBox {
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
Label title = new Label("At " + time);
HBox titleBox = new HBox(title);
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
getChildren().add(titleBox);
double maxMvB = getMaxMvB(seriesList);
for(int i = seriesList.size() - 1; i >= 0; i--) {
Series<String, Number> series = seriesList.get(i);
for(XYChart.Data<String, Number> data : series.getData()) {
if(data.getXValue().equals(category)) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
if(mvb >= 0.01) {
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
double kvb = data.getYValue().doubleValue() / 1000;
double mvb = kvb / 1000;
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
Label label = new Label(series.getName() + ": " + amount);
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
@@ -51,6 +51,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private final Wallet wallet;
private WalletNode walletNode;
private boolean electrumSignatureFormat;
private boolean closed;
/**
* Verification only constructor
@@ -102,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);
@@ -119,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:");
@@ -177,7 +180,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
if(buttons.length > 0) {
dialogPane.getButtonTypes().addAll(buttons);
@@ -240,9 +243,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
EventManager.get().unregister(this);
if(!closed) {
EventManager.get().unregister(this);
closed = true;
}
});
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
@@ -277,8 +284,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
getAddress();
return true;
Address address = getAddress();
return address.getScriptType() != ScriptType.P2TR;
} catch (InvalidAddressException e) {
return false;
}
@@ -311,7 +318,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
String signatureText = privKey.signMessage(message.getText().trim(), scriptType, null);
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
signature.clear();
signature.appendText(signatureText);
privKey.clear();
@@ -394,7 +401,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
@@ -0,0 +1,212 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.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);
setContentDisplay(ContentDisplay.LEFT);
setGraphicTextGap(8);
getStyleClass().add("mixstatus-cell");
}
@Override
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
super.updateItem(mixStatus, empty);
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
if(empty || mixStatus == null) {
setText(null);
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
private void setMixSuccess(Utxo nextMixUtxo) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason != MixFailReason.CANCEL) {
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() + (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 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();
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);
}
}
private ProgressIndicator getProgressIndicator() {
ProgressIndicator progressIndicator;
if(getGraphic() instanceof ProgressIndicator) {
progressIndicator = (ProgressIndicator)getGraphic();
} else {
progressIndicator = new ProgressBar();
}
return progressIndicator;
}
private static Glyph getMixGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getStopGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
public static Glyph getFailGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.getStyleClass().add("fail-warning");
failGlyph.setFontSize(12);
return failGlyph;
}
private static class MixStatusContextMenu extends ContextMenu {
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(isMixing) {
MenuItem mixStop = new MenuItem("Stop Mixing");
if(pool != null) {
mixStop.disableProperty().bind(pool.mixingProperty().not());
}
mixStop.setGraphic(getStopGlyph());
mixStop.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mixStop(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixStop);
} else {
MenuItem mixNow = new MenuItem("Mix Now");
if(pool != null) {
mixNow.disableProperty().bind(pool.mixingProperty().not());
}
mixNow.setGraphic(getMixGlyph());
mixNow.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mix(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixNow);
}
}
}
}
@@ -20,6 +20,7 @@ import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.*;
import javafx.util.Callback;
import org.controlsfx.control.textfield.AutoCompletionBinding;
@@ -37,6 +38,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
@@ -163,7 +165,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNext(wordEntries.get(i + 1).getEditor());
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
@@ -171,7 +174,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNext(passphraseEntry.getEditor());
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
@@ -259,7 +262,13 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
invalidLabel.setText("Unsupported Electrum seed");
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
} else {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
}
@@ -382,6 +391,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
@@ -392,7 +409,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
@@ -401,7 +418,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private Node next;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
@@ -411,7 +429,29 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField = new TextField() {
@Override
public void paste() {
Clipboard clipboard = Clipboard.getSystemClipboard();
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
String[] words = clipboard.getString().split("[\\n\\s]");
WordEntry entry = WordEntry.this;
for(String word : words) {
if(entry.nextField != null) {
entry.nextField.requestFocus();
}
entry.wordField.setText(word);
entry = entry.nextEntry;
if(entry == null) {
break;
}
}
} else {
super.paste();
}
}
};
wordField.setMaxWidth(100);
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
@@ -431,8 +471,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if (next != null) {
next.requestFocus();
if(nextField != null) {
nextField.requestFocus();
}
});
@@ -454,8 +494,12 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return wordField;
}
public void setNext(Node node) {
next = node;
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
@@ -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);
}
}
@@ -33,9 +33,9 @@ public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 100;
private static final int MAX_FRAGMENT_LENGTH = 250;
private static final int ANIMATION_PERIOD_MILLIS = 400;
private static final int ANIMATION_PERIOD_MILLIS = 200;
private static final int QR_WIDTH = 480;
private static final int QR_HEIGHT = 480;
@@ -232,7 +232,7 @@ public class QRDisplayDialog extends Dialog<UR> {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -1,9 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamEvent;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
@@ -13,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;
@@ -41,6 +40,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
@@ -74,6 +74,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
@@ -82,7 +84,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
@@ -104,6 +106,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
Platform.runLater(() -> {
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
if(device.getName().equals(Config.get().getWebcamDevice())) {
webcamDeviceProperty.set(device);
}
}
}
});
});
VBox vBox = new VBox(20);
@@ -122,6 +133,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
Config.get().setWebcamDevice(null);
}
final Throwable result = exception;
@@ -134,6 +148,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
webcamService.cancel();
});
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
Config.get().setWebcamDevice(newValue.getName());
if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel();
}
});
setOnCloseRequest(event -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
@@ -146,7 +166,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
AppServices.moveToActiveWindowScreen(this);
@@ -228,7 +249,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//ignore, bytes not parsable as tx
}
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
result = new Result(complete);
}
} else {
PSBT psbt;
@@ -460,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);
}
@@ -480,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);
}
@@ -496,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);
@@ -520,6 +535,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
@@ -558,10 +574,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
});
button = hd;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
devicesCombo.setConverter(new StringConverter<>() {
@Override
public String toString(WebcamDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
}
@Override
public WebcamDevice fromString(String string) {
throw new UnsupportedOperationException();
}
});
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
button = devicesCombo;
} else {
button = super.createButton(buttonType);
}
if(button instanceof Region) {
((Region)button).setPrefWidth(150);
((Region)button).setMaxWidth(150);
}
button.disableProperty().bind(webcamService.openingProperty());
return button;
}
@@ -570,7 +608,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -745,6 +783,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
public static class WebcamOpenException extends ScanException {
public WebcamOpenException() {
super();
}
public WebcamOpenException(String message) {
super(message);
}
public WebcamOpenException(Throwable cause) {
super(cause);
}
public WebcamOpenException(String message, Throwable cause) {
super(message, cause);
}
}
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
public long calculateDelay(long snapshotDuration, double deviceFps) {
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
@@ -0,0 +1,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);
}
}
}
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.concurrent.Worker;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.controlsfx.dialog.ProgressDialog;
public class ServiceProgressDialog extends ProgressDialog {
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
super(worker);
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle(title);
setHeaderText(header);
dialogPane.getStyleClass().remove("progress-dialog");
Image image = new Image(imagePath);
dialogPane.setGraphic(new ImageView(image));
}
}
@@ -27,7 +27,11 @@ public class TextAreaDialog extends Dialog<String> {
this("");
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
public TextAreaDialog(String defaultValue) {
this(defaultValue, true);
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
final DialogPane dialogPane = new TextAreaDialogPane();
setDialogPane(dialogPane);
@@ -39,6 +43,7 @@ public class TextAreaDialog extends Dialog<String> {
this.textArea.setMaxWidth(Double.MAX_VALUE);
this.textArea.setWrapText(true);
this.textArea.getStyleClass().add("fixed-width");
this.textArea.setEditable(editable);
hbox.getChildren().add(textArea);
HBox.setHgrow(this.textArea, Priority.ALWAYS);
@@ -49,10 +54,13 @@ public class TextAreaDialog extends Dialog<String> {
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dialogPane.getButtonTypes().add(ButtonType.OK);
if(editable) {
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(scanButtonType);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(scanButtonType);
}
Platform.runLater(textArea::requestFocus);
@@ -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;
}
}
@@ -0,0 +1,90 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class TorStatusLabel extends Label {
private static final Logger log = LoggerFactory.getLogger(TorStatusLabel.class);
private final TorConnectionTest torConnectionTest = new TorConnectionTest();
public TorStatusLabel() {
getStyleClass().add("tor-status");
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
setGraphic(getIcon());
update();
}
public void update() {
boolean proxyInUse = AppServices.isUsingProxy();
boolean internal = AppServices.isTorRunning();
if(!proxyInUse || internal) {
torConnectionTest.cancel();
if(internal) {
setTooltip(new Tooltip("Internal Tor proxy enabled"));
}
} else if(!torConnectionTest.isRunning()) {
torConnectionTest.setPeriod(Duration.seconds(20.0));
torConnectionTest.setBackoffStrategy(null);
torConnectionTest.setOnSucceeded(workerStateEvent -> {
getStyleClass().remove("failure");
setTooltip(new Tooltip("External Tor proxy enabled"));
});
torConnectionTest.setOnFailed(workerStateEvent -> {
if(!getStyleClass().contains("failure")) {
getStyleClass().add("failure");
}
setTooltip(new Tooltip("External Tor proxy error: " + workerStateEvent.getSource().getException().getMessage()));
log.warn("Failed to connect to external Tor proxy: " + workerStateEvent.getSource().getException().getMessage());
});
torConnectionTest.start();
}
}
private Node getIcon() {
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
adjust.setRotate(180);
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
bullseye.setFontSize(14);
Group group = new Group();
group.getChildren().addAll(adjust, bullseye);
return group;
}
private static class TorConnectionTest extends ScheduledService<Void> {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() throws IOException {
HostAndPort proxyHostAndPort = HostAndPort.fromString(Config.get().getProxyServer());
Socket socket = new Socket(proxyHostAndPort.getHost(), proxyHostAndPort.getPort());
socket.close();
return null;
}
};
}
}
}
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.protocol.*;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
@@ -25,6 +26,7 @@ public class TransactionHexArea extends CodeArea {
private static final int TRUNCATE_AT = 30000;
private static final int SEGMENTS_INTERVAL = 250;
private String fullHex;
private List<TransactionSegment> previousSegmentList = new ArrayList<>();
public TransactionHexArea() {
@@ -37,7 +39,7 @@ public class TransactionHexArea extends CodeArea {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
transaction.bitcoinSerializeToStream(baos);
String fullHex = Utils.bytesToHex(baos.toByteArray());
fullHex = Utils.bytesToHex(baos.toByteArray());
String hex = fullHex;
if(hex.length() > TRUNCATE_AT) {
hex = hex.substring(0, TRUNCATE_AT);
@@ -242,6 +244,18 @@ public class TransactionHexArea extends CodeArea {
};
}
@Override
public void copy() {
IndexRange selection = getSelection();
if(fullHex != null && selection.getLength() == getLength()) {
ClipboardContent content = new ClipboardContent();
content.putString(fullHex);
Clipboard.getSystemClipboard().setContent(content);
} else {
super.copy();
}
}
private static class TransactionSegment {
public TransactionSegment(int start, int length, Integer index, Integer witnessIndex, String style) {
this.start = start;
@@ -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) + "..." : 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) {
@@ -7,6 +7,7 @@ import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator;
import java.util.List;
public class UtxosTreeTable extends CoinTreeTable {
@@ -38,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
});
getColumns().add(outputCol);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
});
getColumns().add(addressCol);
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
});
mixStatusCol.setCellFactory(p -> new MixStatusCell());
mixStatusCol.setSortable(true);
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
getColumns().add(mixStatusCol);
} else {
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
getColumns().add(addressCol);
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
@@ -93,7 +101,9 @@ public class UtxosTreeTable extends CoinTreeTable {
public void updateHistory(List<WalletNode> updatedNodes) {
//Utxo entries should have already been updated, so only a resort required
sort();
if(!getRoot().getChildren().isEmpty()) {
sort();
}
}
public void updateLabel(Entry entry) {
@@ -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 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 SeedSigner(), 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 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);
}
}
@@ -53,6 +53,10 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
final VBox content = new VBox(20);
name = (CustomTextField)TextFields.createClearableTextField();
name.setText(initialName);
name.setTextFormatter(new TextFormatter<>((change) -> {
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
return change;
}));
content.getChildren().add(name);
HBox existingBox = new HBox(10);
@@ -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;
@@ -14,6 +14,7 @@ import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -312,6 +313,23 @@ public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess
return this.fps;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
WebcamScanDevice that = (WebcamScanDevice) o;
return Objects.equals(fullname, that.fullname);
}
@Override
public int hashCode() {
return Objects.hash(fullname);
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
@@ -3,20 +3,43 @@ package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
private static boolean rescan;
@Override
public List<WebcamDevice> getDevices() {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
if(rescan || webcamDevices.isEmpty()) {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
scanDevices.add(scanDevice);
}
}
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
newDevices.removeAll(webcamDevices);
webcamDevices.addAll(newDevices);
webcamDevices.removeIf(device -> !scanDevices.contains(device));
}
return scanDevices;
return webcamDevices;
}
public static ObservableList<WebcamDevice> getFoundDevices() {
return webcamDevices;
}
public static void rescan() {
rescan = true;
}
}
@@ -1,12 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -16,33 +15,40 @@ 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> {
private WebcamResolution resolution;
private WebcamDevice device;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
private Webcam cam;
private long lastQrSampleTime;
private final Reader qrReader;
static {
Webcam.setDriver(new WebcamScanDriver());
}
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution;
this.device = device;
this.listener = listener;
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader();
}
@Override
@@ -58,6 +64,23 @@ public class WebcamService extends ScheduledService<Image> {
}
cam = webcams.get(0);
if(device != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(device.getName())) {
cam = webcam;
}
}
} else if(Config.get().getWebcamDevice() != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
cam = webcam;
}
}
}
device = cam.getDevice();
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
@@ -69,12 +92,20 @@ public class WebcamService extends ScheduledService<Image> {
opening.set(false);
}
BufferedImage bimg = cam.getImage();
Image image = SwingFXUtils.toFXImage(bimg, null);
BufferedImage originalImage = cam.getImage();
if(originalImage == null) {
return 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();
}
@@ -101,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 = new MultiFormatReader().decode(bitmap);
resultProperty.set(result);
} catch(NotFoundException e) {
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();
}
@@ -133,6 +195,14 @@ public class WebcamService extends ScheduledService<Image> {
this.resolution = resolution;
}
public WebcamDevice getDevice() {
return device;
}
public void setDevice(WebcamDevice device) {
this.device = device;
}
public boolean isOpening() {
return opening.get();
}
@@ -140,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,27 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class ChildWalletAddedEvent extends WalletChangedEvent {
private final Storage storage;
private final Wallet childWallet;
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
super(masterWallet);
this.storage = storage;
this.childWallet = childWallet;
}
public Storage getStorage() {
return storage;
}
public Wallet getChildWallet() {
return childWallet;
}
public String getMasterWalletId() {
return storage.getWalletId(getWallet());
}
}
@@ -0,0 +1,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;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class MixToConfigChangedEvent {
private final Wallet wallet;
public MixToConfigChangedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -6,21 +6,27 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class NewWalletTransactionsEvent {
private final Wallet wallet;
private final List<BlockTransaction> blockTransactions;
private final List<TransactionEntry> transactionEntries;
private final long totalBlockchainValue;
private final long totalMempoolValue;
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
public NewWalletTransactionsEvent(Wallet wallet, List<TransactionEntry> transactionEntries) {
this.wallet = wallet;
this.blockTransactions = blockTransactions;
this.totalBlockchainValue = totalBlockchainValue;
this.totalMempoolValue = totalMempoolValue;
this.transactionEntries = transactionEntries;
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
}
public Wallet getWallet() {
@@ -28,7 +34,7 @@ public class NewWalletTransactionsEvent {
}
public List<BlockTransaction> getBlockTransactions() {
return blockTransactions;
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
public long getTotalValue() {
@@ -55,4 +61,22 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream()
.filter(txEntry -> txEntry.getValue() == 0 && txEntry.isConfirming() && !allOutputsSpent(txEntry))
.map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
private boolean allOutputsSpent(TransactionEntry txEntry) {
return txEntry.getChildren().stream()
.map(refEntry -> ((TransactionHashIndexEntry)refEntry))
.filter(txRefEntry -> txRefEntry.getType() == HashIndexEntry.Type.OUTPUT)
.allMatch(txRefEntry -> txRefEntry.getHashIndex().isSpent());
}
}
@@ -1,20 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveActionEvent {
private final Wallet wallet;
public class ReceiveActionEvent extends FunctionActionEvent {
public ReceiveActionEvent(NodeEntry receiveEntry) {
this.wallet = receiveEntry.getWallet();
super(Function.RECEIVE, receiveEntry.getWallet());
}
public ReceiveActionEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
super(Function.RECEIVE, wallet);
}
}
@@ -1,22 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.net.Aopp;
public class ReceiveProofEvent {
private final Wallet wallet;
private final Aopp aopp;
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
this.wallet = wallet;
this.aopp = aopp;
}
public Wallet getWallet() {
return wallet;
}
public Aopp getAopp() {
return aopp;
}
}
@@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
public class SelectEntryEvent {
private final Entry entry;
public SelectEntryEvent(Entry entry) {
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
public Wallet getWallet() {
return entry.getWallet();
}
}
@@ -2,23 +2,24 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import java.util.List;
public class SendActionEvent {
private final Wallet wallet;
public class SendActionEvent extends FunctionActionEvent {
private final List<BlockTransactionHashIndex> utxos;
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this.wallet = wallet;
super(Function.SEND, wallet);
this.utxos = utxos;
}
public Wallet getWallet() {
return wallet;
}
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
}
@Override
public boolean selectFunction() {
return !getUtxos().isEmpty();
}
}
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class SorobanInitiatedEvent {
private Wallet wallet;
public SorobanInitiatedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -10,8 +11,10 @@ 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;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, false);
@@ -21,8 +24,20 @@ 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, 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;
}
public Wallet getWallet() {
@@ -37,6 +52,10 @@ public class SpendUtxoEvent {
return payments;
}
public List<byte[]> getOpReturns() {
return opReturns;
}
public Long getFee() {
return fee;
}
@@ -44,4 +63,8 @@ public class SpendUtxoEvent {
public boolean isIncludeSpentMempoolOutputs() {
return includeSpentMempoolOutputs;
}
public Pool getPool() {
return pool;
}
}
@@ -1,12 +1,16 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.io.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class StorageEvent extends TimedEvent {
private static final Logger log = LoggerFactory.getLogger(StorageEvent.class);
private static boolean firstRunDone = false;
private static final Map<String, Long> eventTime = new HashMap<>();
@@ -22,8 +26,10 @@ public class StorageEvent extends TimedEvent {
eventTime.put(walletId, System.currentTimeMillis());
timeMills = keyDerivationPeriod;
} else if(action == Action.END) {
long start = eventTime.get(walletId);
if(firstRunDone) {
Long start = eventTime.get(walletId);
if(start == null) {
log.error("Could not find start event time for wallet id " + walletId);
} else if(firstRunDone) {
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
}
@@ -0,0 +1,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;
}
}

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