Compare commits

...
241 Commits
Author SHA1 Message Date
Craig Raw b2657cdcfb v1.5.0 final 2021-09-23 15:52:33 +02:00
Craig Raw 6bbae204a6 restrict mixing to mainnet and testnet 2021-09-23 15:30:13 +02:00
Craig Raw 0b55dd8a1e ui fixes to mix start and stop 2021-09-23 14:18:37 +02:00
Craig Raw f74287697c use master wallet passphrase for child wallets when loading 2021-09-23 12:49:06 +02:00
Craig Raw 7a3e1dfa1f change default log level to warn 2021-09-23 12:28:47 +02:00
Craig Raw 427a6925ee mainnet mixing, improve pools selection, other fixes 2021-09-23 10:59:40 +02:00
Craig Raw c55b19af0f tor status windows visual fixes 2021-09-21 11:55:23 +02:00
Craig Raw e12f7a634a add tor status indicator to status bar 2021-09-21 11:22:09 +02:00
Craig Raw ca1f934138 update README for https cloning 2021-09-20 16:42:55 +02:00
Craig Raw 4f9b87b74e fix recursive clone via https 2021-09-20 16:37:03 +02:00
Craig Raw e1e5df78c6 fix text truncation in whirlpool dialog on windows 2021-09-20 15:51:57 +02:00
Craig Raw cfd06a8513 use cached tx0previews, only save mixconfig on apply 2021-09-20 15:30:13 +02:00
craigrawandGitHub 1c1099217b Merge pull request #198 from zeroleak/whirlpool-client-0.23.30-early5
update to whirlpool-client 0.23.31
2021-09-20 13:10:47 +02:00
zeroleak 6939d8a06a upgrade to whirlpool-client 0.23.33 2021-09-18 11:41:52 +02:00
zeroleak dbebade3ab update to whirlpool-client 0.23.31 2021-09-11 08:17:15 +02:00
Craig Raw 5895837b60 hide receive tab on whirlpool wallets 2021-09-10 15:35:40 +02:00
Craig Raw 0f9e9d9c35 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-09-10 13:58:47 +02:00
Craig Raw 59452e64ea add additional required packages 2021-09-10 13:58:18 +02:00
craigrawandGitHub 573d5b2c18 Merge pull request #195 from RequestPrivacy/patch-1
Fix skdman link
2021-09-10 10:01:47 +02:00
Craig Raw 2f0dc5bea8 ensure submodule is also on correct tag 2021-09-10 09:41:30 +02:00
RequestPrivacyandGitHub 27f7d5ad38 Fix skdman link 2021-09-09 16:16:54 +00:00
Craig Raw 414ffc2648 add first draft of reproducible build documentation 2021-09-09 14:07:45 +02:00
Craig Raw aaca9ffa16 dont allow receiving on badbank wallet, introduce skipInstaller flag in build 2021-09-08 13:22:21 +02:00
Craig Raw 44b9cee825 fix outdated reference to Java 14 and link to Java 16 JDK 2021-09-07 19:27:22 +02:00
Craig Raw c024c351ac minor fixes 2021-09-07 11:51:14 +02:00
Craig Raw ed2914f2aa replace illegal filename characters in wallet name with underscores 2021-09-06 14:49:53 +02:00
Craig Raw 0b4785e01c v1.5.0 beta1 2021-09-06 11:53:34 +02:00
Craig Raw 0c50c9cb9c make jackson jars open and non-synthetic modules 2021-09-06 10:18:36 +02:00
Craig Raw 916c2b6122 fix module issues in binary 2021-09-05 15:47:15 +02:00
Craig Raw 308a89c958 update required java to 16 in build instructions 2021-09-03 20:51:56 +02:00
Craig Raw 88ebef97d4 support mixing from all single sig wallets, handle tor proxy change, and other minor fixes 2021-09-03 17:16:37 +02:00
Craig Raw a42761981c support mixing to multisig wallets 2021-09-02 17:14:01 +02:00
Craig Raw b6f047d382 minor fixes 2021-09-02 15:26:16 +02:00
Craig Raw 8f63d89be8 avoid creating a core wallet if no wallets are open 2021-09-02 15:22:53 +02:00
Craig Raw 6e6111b47c support using stored mix indexes to avoid resending a utxo to the coordinator 2021-09-02 14:12:59 +02:00
Craig Raw e8af7c70bd refactor to WhirlpoolServices 2021-09-02 12:37:07 +02:00
Craig Raw 2fc551e35b add mix to functionality 2021-09-02 11:39:56 +02:00
Craig Raw adb77771aa add mix config persistence and initial usage 2021-09-01 13:10:46 +02:00
Craig Raw 13e01451b7 update nightjar for tor identity change functionality 2021-08-31 16:43:56 +02:00
Craig Raw aa10bcfe1a fixes for encrypted whirlpool wallets and other issues 2021-08-31 16:19:24 +02:00
craigrawandGitHub f30c00ba8f Merge pull request #183 from zeroleak/whirlpool-client-0.23.30-early4
Upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3
2021-08-31 10:57:49 +02:00
zeroleak 4577a64ad5 apply Craig's feedback
- use V2__Whirlpool.sql
- allow pools list & tx0 preview without seed
- filter DataSource wallet for refresh
2021-08-29 10:49:25 +02:00
Craig Raw 7371ca2994 add option to optimize transactions for privacy and display privacy analysis 2021-08-27 16:00:17 +02:00
zeroleak 050c4fc31e upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-27 09:20:41 +02:00
zeroleak 772370808c Merge remote-tracking branch 'remotes/origin/master' into whirlpool-client-0.23.30-early4
# Conflicts:
#	src/main/java/com/sparrowwallet/sparrow/AppServices.java
#	src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java
2021-08-24 12:18:54 +02:00
Craig Raw b22e891b7d make select all, copy on transaction hex area copy untruncated transaction hex 2021-08-24 10:52:56 +02:00
Craig Raw 57b3214c54 improvements to whirlpool dialog 2021-08-24 10:40:27 +02:00
Craig Raw 615b78b497 fix mix event handling for multiple wallets 2021-08-24 09:29:54 +02:00
Craig Raw d1ab1db1c5 fix issues with subtabs in new wallets 2021-08-23 17:27:30 +02:00
Craig Raw f5ac6a3b73 support whirlpool data storage in wallet file, add mixing ui 2021-08-23 16:36:02 +02:00
zeroleak fec45356a2 upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-22 11:26:11 +02:00
Craig Raw 37c4ff4dd7 reduce max number multisig keystores 2021-08-16 17:09:21 +02:00
Craig Raw b0877d94bf make all wallet addresses non-editable once child wallets are added 2021-08-16 13:33:05 +02:00
Craig Raw a3e4342d7d save app width and height across restarts 2021-08-16 11:25:18 +02:00
Craig Raw 90355a653f followup 2021-08-16 11:04:46 +02:00
Craig Raw 6339c3a7d7 allow multisig wallets with many (up to 50) keystores 2021-08-16 10:46:34 +02:00
Craig Raw 8f7f0d4c61 truncate labels over 255 chars before persisting to db 2021-08-16 10:15:14 +02:00
Craig Raw 74b4f51640 add menu items to copy psbt to clipboard in hex or base64 2021-08-16 08:48:14 +02:00
Craig Raw 2caee79df4 initial whirlpool integration 2021-08-12 17:50:13 +02:00
Craig Raw 34b4c39ccd add logging when event wallet id is not present 2021-08-04 10:45:01 +02:00
Craig Raw 34d1571669 fix aopp verification regression on button type 2021-08-04 10:03:18 +02:00
Craig Raw 22465b1b65 followup 2021-08-02 16:10:47 +02:00
Craig Raw b91e8eab51 fix tab loading animation for subtabbed wallets 2021-07-30 15:13:58 +02:00
Craig Raw 55e69bf263 fix naming when using a subtab wallet, import and export wallets with child wallets with db persistence on sparrow exporter 2021-07-30 14:17:21 +02:00
Craig Raw 02d3817cb1 introduce wallet subtabs 2021-07-30 09:46:42 +02:00
Craig Raw fc9cdaabb4 export UTXOs to CSV 2021-07-29 12:09:54 +02:00
Craig Raw 2aa3d83402 fix using the correct address label for the utxo label when recieving a batched tx to several labelled address 2021-07-29 11:40:15 +02:00
Craig Raw fc5d6ada36 add caching for verbose transaction lookups to avoid repeat server requests 2021-07-29 11:09:49 +02:00
Craig Raw be599fb003 fix hummingbird module definition 2021-07-21 15:30:46 +02:00
Craig Raw d79c4a13f6 upgrade to hummingbird v1.6.2 2021-07-20 11:47:56 +02:00
Craig Raw e6fce14fde handle unknown derivation path for unchained caravan key 2021-07-19 11:56:02 +02:00
Craig Raw 5c3a00b71b v1.4.3 2021-07-15 12:02:45 +02:00
Craig Raw ced4d4d337 add caravan import/export, minor ui fixes 2021-07-15 11:09:39 +02:00
Craig Raw 4a3ad9f4ff restrict shown sighash to available values 2021-07-14 15:13:38 +02:00
Craig Raw f5a72105ac minor fixes after refactoring 2021-07-12 08:56:37 +02:00
Craig Raw 0502eec0cd dont show message sign context menu item when message signing cant be performed 2021-07-09 13:35:45 +02:00
Craig Raw 8e6933b5ca increase animation rate and fragment length for UR QRs 2021-07-09 08:23:40 +02:00
Craig Raw 1fd1dec6cf rename tx segwit version field to segwit flag 2021-07-08 11:46:21 +02:00
Craig Raw 422713ff53 fix camera selection issues on linux 2021-07-05 12:58:26 +02:00
Craig Raw ada45ee75b allow selection of webcam from QR scan dialog 2021-07-05 12:33:31 +02:00
Craig Raw 2f153686dd avoid hang on closing webcam due to rescans for new camera devices 2021-07-04 19:18:02 +02:00
Craig Raw f691f1691e add missing requires directive for bwt lib 2021-07-02 13:41:22 +02:00
Craig Raw 1f9e37b40c enable max button for selected utxos without address and label filled 2021-07-02 12:05:38 +02:00
Craig Raw a1d2de1859 use all addressable script types (not just single hash types) 2021-07-02 10:44:24 +02:00
Craig Raw 143472bdfc fix save of address labels on a new wallet 2021-07-01 09:41:56 +02:00
Craig Raw b9e64d42ff accept pasting a whitespace delimited sequence of words into
a textfield of mnemonic word entry
2021-06-30 11:55:58 +02:00
Craig Raw 9a09bb8cda always delete hwi dir on osx before copying 2021-06-30 11:51:31 +02:00
Craig Raw 4b028af123 handle multiple selection when freezing and unfreezing utxos 2021-06-30 09:18:14 +02:00
Craig Raw 8033e5fd88 improve amount error labels adding dust threshold label for too low amounts 2021-06-29 12:48:02 +02:00
Craig Raw badf8c8f2f optimize and increase sampling rate of qr reading 2021-06-29 10:53:22 +02:00
Craig Raw b6a353815c switch mempool size chart to kvB when max Y value is less than 1 MvB 2021-06-28 14:14:22 +02:00
Craig Raw ea2f858dc9 close message sign dialog on platform button and escape key 2021-06-28 13:32:24 +02:00
Craig Raw 324540009a fix detection of input type on finalizing psbt wallet 2021-06-28 13:08:44 +02:00
Craig Raw 1c1f90344f update github build to java 16 2021-06-25 15:18:51 +02:00
Craig Raw 094dd45547 upgrade to gradle 7.1, java 16, javafx 16 2021-06-25 14:56:22 +02:00
Craig Raw 6d434722cc fix prev commit 2021-06-24 12:41:32 +02:00
Craig Raw c8a4ed0c3d add documentation link to bitcoin core error message 2021-06-24 10:07:54 +02:00
Craig Raw c8d997fbf0 v1.4.2 2021-06-23 12:51:38 +02:00
Craig Raw 911ed3a718 improve background text 2021-06-23 12:30:58 +02:00
Craig Raw dbfed31432 add format toggle to message signing dialog (electrum or trezor) 2021-06-22 16:00:15 +02:00
Craig Raw 6f3d4e224e add context menu item to copy transaction hex 2021-06-22 11:47:56 +02:00
Craig Raw 4d6609990c fix error messages on subclassed importers/exporters 2021-06-22 10:13:37 +02:00
Craig Raw 5482196cc7 dark theme improvements 2021-06-22 08:36:16 +02:00
Craig Raw 09f6c9ef81 ensure cleanup of migrated wallets when importing 2021-06-21 11:50:58 +02:00
Craig Raw 9b8f97c041 fix import of sparrow wallet with seed, alphabetically sort import and export choices 2021-06-21 11:25:47 +02:00
Craig Raw c68c713a4b add orig files for keystone and seedsigner 2021-06-18 13:31:08 +02:00
Craig Raw 02e144f802 set keystore label, model and source type when importing a wallet from specter desktop 2021-06-18 11:21:53 +02:00
Craig Raw a9ab4d6c78 Handle importing a wallet from a crypto-account QR using the File > Import Wallet dialog 2021-06-17 14:34:11 +02:00
Craig Raw 5df4e5761c add seedsigner keystore import 2021-06-16 14:46:55 +02:00
Craig Raw 6d8b8579ba followup 2021-06-16 09:37:35 +02:00
Craig Raw eaa5190502 various db persistence improvements 2021-06-16 09:35:26 +02:00
Craig Raw 445db6a4d6 reduce file reads on db files to avoid locking exception on windows 2021-06-15 17:58:25 +02:00
Craig Raw 7f178b5f67 jpms related changes for v1.4.2-beta 2021-06-15 16:50:05 +02:00
Craig Raw 1208baf00e use mempool.space onion address for fee rates if tor proxy enabled 2021-06-15 09:31:12 +02:00
Craig Raw 655a473cd5 update send selected button when freezing or unfreezing utxos 2021-06-14 16:58:05 +02:00
Craig Raw e6c536930b highlight default button and allow actioning from keyboard when only one usb device is listed 2021-06-14 16:55:01 +02:00
Craig Raw f1510de360 update encrypted seeds and private keys when wallet password changes 2021-06-14 14:54:40 +02:00
Craig Raw cfac2768ae use varbinary rather than fixed binary column types where length is variable 2021-06-14 11:52:12 +02:00
Craig Raw ab41f2e80e upgrade to h2 HEAD 2021-06-11 15:45:16 +02:00
Craig Raw 9ebabecfbe use json persistence for sparrow wallet export, add sparrow wallet import to wallets dir 2021-06-11 11:29:54 +02:00
Craig Raw 8914acff68 minor followup 2021-06-10 17:02:32 +02:00
Craig Raw 4a0ecba716 add keystone hww import and export 2021-06-10 16:37:41 +02:00
Craig Raw e99b1d4171 upgrade to hwi 2.0.2 and relocate hwi to sparrow home folder on osx to avoid partial installation deletions in tmpdir 2021-06-10 14:06:14 +02:00
Craig Raw a59d5d3086 introduce database persistence with automatic migration of existing wallets 2021-06-10 12:08:35 +02:00
Craig Raw 600a77da3a allow psbts without utxo data to be loaded if utxos are provided in an existing psbt 2021-05-28 11:09:23 +02:00
craigrawandGitHub bc83f6fa22 Merge pull request #134 from haakonn/mnemonic-entry
Make mnemonic entry more efficient
2021-05-27 16:18:39 +02:00
Haakon Nilsen 4cbde7e7aa When entering mnemonic words, don't close dropdown when a prefix is encountered, and move focus to the next field upon completion 2021-05-27 10:56:47 +02:00
craigrawandGitHub 3ae63408e6 Merge pull request #133 from haakonn/save-tx-menuitems
Fixes around transaction saving menu items
2021-05-27 09:58:11 +02:00
Haakon Nilsen e740c6d162 Disable transaction saving menu items after a transaction tab is closed and no other tabs are open 2021-05-27 09:15:36 +02:00
Haakon Nilsen f7f5852476 Disable the "Save transaction" menu item when starting app without any open tabs 2021-05-26 18:57:07 +02:00
Haakon Nilsen 447e2ab264 Disable the keyboard shortcut for saving PSBT binary when its parent menu is disabled 2021-05-26 18:54:20 +02:00
craigrawandGitHub 8a77f22158 Merge pull request #132 from haakonn/close-on-escape
Close "About" and "Introduction" when Escape key is pressed
2021-05-26 08:39:16 +02:00
Haakon Nilsen c096327be4 Close "About" and "Introduction" when Escape key is pressed 2021-05-25 22:13:51 +02:00
Craig Raw 911153e1aa fix receive to address context menu actions 2021-05-25 08:21:17 +02:00
Craig Raw a60eadf8fc add export to specter diy 2021-05-24 14:20:08 +02:00
Craig Raw 9ebbf2557f support electrs batching and improve batching read timeout handling 2021-05-24 13:39:06 +02:00
Craig Raw 42b279d22a add specter desktop reimport usb wallets warning 2021-05-24 12:29:57 +02:00
Craig Raw 1a452db4cf add tooltip to send utxos button with directions on how to select multiple utxos 2021-05-24 12:27:30 +02:00
Craig Raw c1cf5be616 add mempool.bisq.services as a broadcaster, broadcast tx twice if possible on mainnet, handle different network broadcaster network capabilities 2021-05-21 11:48:13 +02:00
Craig Raw dd146210ba set send amount to total utxo value when sending selected utxos before an address or label is added 2021-05-21 09:53:00 +02:00
Craig Raw 94088f795c handle invalid values from mempool.get_fee_histogram 2021-05-21 08:44:55 +02:00
Craig Raw c5b09189df explicitly place decorationpane in the scene graph to avoid app resizing issues 2021-05-20 17:27:33 +02:00
Craig Raw 197c44bb07 update null and empty labels, clear script hash cache on increasing gap limit 2021-05-20 14:49:16 +02:00
Craig Raw c202a941b9 add mempool.emzy.de as another broadcaster 2021-05-20 14:48:01 +02:00
Craig Raw 383594b03c v1.4.1 2021-05-20 12:32:15 +02:00
Craig Raw 42bfe572ef calculate and cache script hashes statuses on wallet load to avoid unnecessary initial history fetching 2021-05-20 09:52:02 +02:00
Craig Raw 1677c47500 request payjoin transactions (over tor if available) in background thread 2021-05-19 12:24:46 +02:00
Craig Raw 53a447c72d fix cpu usage on spreadsheetview 2021-05-19 09:51:55 +02:00
Craig Raw f23a891ece add send to many dialog 2021-05-19 08:41:15 +02:00
Craig Raw 0e42c657b3 broadcast transactions over tor to a broadcasting service where tor proxy available 2021-05-18 09:37:59 +02:00
Craig Raw b17c15f702 request new change address in the transaction diagram 2021-05-17 13:21:56 +02:00
Craig Raw c9cdf6e77d handle partial rbf, fix cursor reset on payment label 2021-05-17 11:58:12 +02:00
Craig Raw 4f6ead842d fix txtree selection bug when preselecting a txi/txo on opening the editor for the first time 2021-05-17 10:53:26 +02:00
Craig Raw adcddfa84d various minor utxo screen improvements, including hiding the utxo chart 2021-05-17 10:09:19 +02:00
Craig Raw d67c5c5218 update related address, tx, txi and txo labels when null, set tx, txi, txo labels to address label on receive, add paste label to context menu 2021-05-14 18:03:46 +02:00
Craig Raw fb72010bdf refactor storage to handle different persistence strategies 2021-05-14 12:55:12 +02:00
Craig Raw 8bc8bdb2f2 dont backup wallets outside of wallets folder 2021-05-12 14:52:19 +02:00
Craig Raw 8f4cf9f2a0 add unique4j classes to avoid module dependency issue 2021-05-12 14:28:56 +02:00
Craig Raw ed323a8388 ensure aopp window is always on top 2021-05-12 09:19:49 +02:00
Craig Raw d559b437b3 bring window to front one external file load 2021-05-12 09:02:39 +02:00
Craig Raw 6155306acc always try obtain application lock on startup 2021-05-12 08:46:24 +02:00
Craig Raw 6f35d86890 bring opened file window to foreground 2021-05-11 12:58:11 +02:00
Craig Raw cbf32a36c6 follow up #2 2021-05-11 12:40:20 +02:00
Craig Raw 9789e0f198 follow up 2021-05-11 12:33:33 +02:00
Craig Raw 574209c837 ensure file and uri opening on original app instance 2021-05-11 12:31:36 +02:00
Craig Raw 74c83fc5e1 fade animation on server toggle while connecting 2021-05-11 08:52:45 +02:00
Craig Raw a4ddc11bab Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-05-10 14:34:57 +02:00
Craig Raw 3bf480234f fix aopp alert dialog on windows 2021-05-10 14:25:16 +02:00
Craig Raw 29811ca5de handle uri and psbt opening on linux 2021-05-10 14:22:24 +02:00
Craig Raw b7522420cf cleanup windows install 2021-05-10 13:41:46 +02:00
Craig Raw f7d1e22e4e use custom wix installation file 2021-05-10 13:39:40 +02:00
Craig Raw e0c38031b1 add aopp registry keys on windows install 2021-05-10 12:17:52 +02:00
Craig Raw e046512e86 open files or uris from commandline arguments 2021-05-10 11:44:06 +02:00
Craig Raw 7cba0de268 check if opening uris is supported 2021-05-10 08:42:50 +02:00
Craig Raw 425e476f20 add aopp proof of address ownership support 2021-05-07 16:24:28 +02:00
Craig Raw e5dd33d5a1 register and add bitcoin: uri handler, register aopp: handler 2021-05-06 14:21:56 +02:00
Craig Raw 07012615ff 2nd fix to alert height 2021-05-05 14:59:53 +02:00
Craig Raw 23f3ae51db hide alert on clicking link 2021-05-05 14:43:33 +02:00
Craig Raw 10514b8039 shorten mitm message to avoid alert height ellipsis 2021-05-05 14:35:43 +02:00
Craig Raw 19637fd706 handle missing webcam dependency on windows showing error alert with link to resolve 2021-05-05 14:31:28 +02:00
Craig Raw dd56e2b42e handle windows with owner only file and directory creation 2021-05-05 12:05:48 +02:00
Craig Raw e0b6530275 only save tmp backup wallet if connected 2021-05-05 11:34:45 +02:00
Craig Raw a0d3f3b745 ensure config, wallet dirs and wallets are owner read/write only 2021-05-05 11:25:25 +02:00
Craig Raw 438c13fe2d add wallet transactions loading log viewer, and improve loading messages 2021-05-04 15:17:57 +02:00
Craig Raw c067300ee8 fix empty addresses table bug after requesting many new addresses 2021-05-04 10:42:18 +02:00
Craig Raw 020ca2f312 always use 8 fraction digits when displaying btc amounts, use a fixed width font for numbers on osx 2021-05-04 10:23:35 +02:00
Craig Raw 5ede1dd97d ensure wallet export file streams are always closed 2021-04-30 14:03:58 +02:00
Craig Raw 45f6ae214a add icons to wallet context menu items where they appear as action buttons 2021-04-30 13:19:53 +02:00
Craig Raw 45a9093055 ensure import file stream is closed 2021-04-29 20:58:04 +02:00
Craig Raw d6c7a0b757 cache fetched script hash status and dont retrieve history again until it changes 2021-04-29 13:21:52 +02:00
Craig Raw 55181165c8 use read timeout to improve disconnection behaviour, especially while refreshing a wallet 2021-04-29 08:02:56 +02:00
Craig Raw b6f7483bf9 v1.4.0 2021-04-28 13:10:06 +02:00
Craig Raw 0593c764b6 remove socket read timeout 2021-04-28 13:03:58 +02:00
Craig Raw 11a3bfd893 make welcome dialog server toggle uneditable and add tooltip to indicate demonstration only 2021-04-28 12:33:18 +02:00
Craig Raw f1d7fc656c set mnemonic enter words button to number of chosen words when dropdown item clicked 2021-04-28 12:18:35 +02:00
Craig Raw ec918b78ed followup to public server change on wallet history failure 2021-04-28 11:46:46 +02:00
Craig Raw 6f5ee7c695 handle server error conditions better, and if using public server try another 2021-04-28 11:23:17 +02:00
Craig Raw e4264d0199 support toggling trezor passphrase on and off in status bar usb menu 2021-04-27 13:56:32 +02:00
Craig Raw 9feefe8203 show file open/save dialogs on the same screen as the currently focussed window 2021-04-27 12:52:42 +02:00
Craig Raw 93f9539f7b show dialogs on the same screen as the currently focussed window 2021-04-27 12:22:39 +02:00
Craig Raw 8865f4946a follow up to previous commit 2021-04-27 09:44:56 +02:00
Craig Raw 65e13d7b50 fix adding new payment tabs, update last used label on connection 2021-04-27 08:17:30 +02:00
Craig Raw 11189bc605 create wallet transaction in background thread to keep ui responsive, and indicate if long running 2021-04-26 16:45:17 +02:00
Craig Raw 40faaec31e handle user defined fee amount better by requiring a fee rate of 1 sat/vb for utxo selectors 2021-04-26 14:40:11 +02:00
Craig Raw b5b384f5da update transaction tree with final txid when transaction is extracted 2021-04-24 09:04:28 +02:00
Craig Raw 23c802bb5f cpfp follow up 2021-04-24 08:40:48 +02:00
Craig Raw 4567f62ef8 hide show derivation link to fix linux width issue 2021-04-23 12:16:20 +02:00
Craig Raw cd00535212 add create cpfp transaction functionality 2021-04-23 12:05:30 +02:00
Craig Raw 3ff626e2aa always set server toggle tooltip on connection to handle bwt startup 2021-04-22 12:24:09 +02:00
Craig Raw deee63c701 dont revert to tx broadcast state when verbose tx fetch fails 2021-04-22 12:12:42 +02:00
Craig Raw b465b9fdaf various minor ui fixes including update screens after includemempooloutputs changed 2021-04-21 13:09:31 +02:00
Craig Raw f377f97931 add submit bug report to help menu 2021-04-21 10:37:41 +02:00
Craig Raw 58bee33383 always display fresh unused address on startup and when address has been used 2021-04-21 10:23:48 +02:00
Craig Raw 18075e0686 improve variable naming for utxo selection criteria 2021-04-21 09:37:19 +02:00
Craig Raw b836774289 if allow unconfirmed, include mempool outputs in balance #2 2021-04-20 11:57:22 +02:00
Craig Raw f2f15208bf if allow unconfirmed, include mempool outputs in balance 2021-04-20 11:55:43 +02:00
Craig Raw 164742387f disable receive address field once used 2021-04-20 11:46:36 +02:00
Craig Raw e62b44bbbe allow external unconfirmed utxos to be spent 2021-04-20 11:08:44 +02:00
Craig Raw e9d0491b42 move insufficient funds label to below amount field 2021-04-20 10:08:07 +02:00
Craig Raw 3fc2127337 import and support master private key keystores 2021-04-20 08:28:16 +02:00
Craig Raw 702d92d4f2 handle failure to retrieve broadcasted transaction from mempool, allow retry 2021-04-19 10:34:47 +02:00
Craig Raw a440c22455 deduct unconfirmed transactions with negative values from balance 2021-04-19 09:50:09 +02:00
Craig Raw 1c2e635650 spacing followup 2021-04-19 09:34:28 +02:00
Craig Raw 7b856f32a2 add insufficient inputs feedback label 2021-04-19 09:29:19 +02:00
Craig Raw 42b96e042b improve bip39 keystore import ux 2021-04-19 08:41:46 +02:00
Craig Raw f8023e9a72 add (non-final) suffix to fee rate field when tx is lacking signatures 2021-04-16 14:05:53 +02:00
Craig Raw 7b9b78684c show tx hex tooltip input, output and witness indexes 2021-04-16 14:00:12 +02:00
Craig Raw 10d6fb8b2b fix npe on fee rates selection toggle 2021-04-16 12:30:59 +02:00
Craig Raw 1556b8930c null-safe testing of device needspin and needspassphrase 2021-04-16 11:38:01 +02:00
Craig Raw d52bade085 various minor ui improvements 2021-04-16 10:53:05 +02:00
Craig Raw 2c98c8606c add transaction hex tooltips 2021-04-15 15:18:00 +02:00
Craig Raw a8430c79eb allow manual configuration of hwi executable path 2021-04-14 11:25:40 +02:00
Craig Raw 3fe5fd89c4 fix qr display dialog sizing 2021-04-14 10:20:23 +02:00
Craig Raw 78406b3110 use cryptopsbt encoding 2021-04-14 09:17:42 +02:00
Craig Raw 2dfdbd6d78 support ur:crypto-output scan and display of wallet output descriptor 2021-04-13 17:27:42 +02:00
Craig Raw 2e86840e92 reduce opacity for unconfirmed txes 2021-04-12 16:27:10 +02:00
Craig Raw 9600d5707c when importing electrum wallets retrieve tx labels from address labels 2021-04-12 16:16:07 +02:00
Craig Raw 497cf333b0 ensure wallet cannot have negative balance with same block tx ordering 2021-04-12 15:47:45 +02:00
Craig Raw eb55b9420a add option to strip sensitive info from psbt serialization 2021-04-12 13:42:36 +02:00
Craig Raw 3b3e46983f deduplicate output descriptors for bwt 2021-04-12 12:22:51 +02:00
Craig Raw 31fb527218 color transaction values which deduct from wallet balance in red 2021-04-12 09:16:49 +02:00
265 changed files with 14754 additions and 1719 deletions
+4 -9
View File
@@ -12,16 +12,11 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 15.0.2
if: ${{ runner.os == 'Linux' }}
uses: joschi/setup-jdk@v2
- name: Set up JDK 16.0.1
uses: actions/setup-java@v2
with:
java-version: 15
- name: Set up JDK 14.0.2
if: ${{ runner.os == 'Windows' }}
uses: actions/setup-java@v1
with:
java-version: 14.0.2
distribution: 'adopt'
java-version: '16.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:sparrowwallet/drongo.git
url = ../../sparrowwallet/drongo.git
+11 -4
View File
@@ -8,11 +8,18 @@ More information (and release binaries) can be found at https://sparrowwallet.co
## Building
To clone this project, use
To clone this project, use
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
These binaries can be built from source using
`./gradlew jpackage`
@@ -20,7 +27,7 @@ When updating to the latest HEAD
`git pull --recurse-submodules`
All jar files created are reproducible builds.
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
## Running
@@ -28,7 +35,7 @@ If you prefer to run Sparrow directly from source, it can be launched with
`./sparrow`
Java 14 or higher must be installed.
Java 16 or higher must be installed.
## Configuration
+385 -24
View File
@@ -1,11 +1,11 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.kordamp.gradle.jdeps' version '0.9.0'
id 'org.beryx.jlink' version '2.22.0'
id 'extra-java-module-info'
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.3.2'
def sparrowVersion = '1.5.0'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -29,7 +29,7 @@ tasks.withType(AbstractArchiveTask) {
}
javafx {
version = "15"
version = "16"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -38,29 +38,46 @@ 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.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.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.5.5')
implementation('com.sparrowwallet:hummingbird:1.6.2')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
}
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.2' ) {
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@@ -69,15 +86,19 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.7')
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.17-SNAPSHOT')
testImplementation('junit:junit:4.12')
}
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.MainApp'
}
compileJava {
options.with {
@@ -96,6 +117,10 @@ processResources {
}
}
test {
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
}
run {
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
@@ -105,6 +130,7 @@ run {
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
"--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=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
@@ -112,7 +138,8 @@ run {
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
@@ -127,12 +154,17 @@ jlink {
requires 'java.xml'
requires 'java.logging'
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
@@ -143,15 +175,23 @@ jlink {
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
"--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=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=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--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-reads=com.sparrowwallet.merged.module=java.desktop"]
"--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=logback.classic",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
@@ -162,11 +202,11 @@ jlink {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/associations.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', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
@@ -203,3 +243,324 @@ task packageTarDistribution(type: Tar) {
include "Sparrow/**"
}
}
extraJavaModuleInfo {
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
exports('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.core.async')
exports('com.fasterxml.jackson.core.base')
exports('com.fasterxml.jackson.core.exc')
exports('com.fasterxml.jackson.core.filter')
exports('com.fasterxml.jackson.core.format')
exports('com.fasterxml.jackson.core.io')
exports('com.fasterxml.jackson.core.json')
exports('com.fasterxml.jackson.core.json.async')
exports('com.fasterxml.jackson.core.sym')
exports('com.fasterxml.jackson.core.type')
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
requires('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.annotation')
}
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
requires('java.desktop')
requires('java.logging')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('java.sql')
requires('java.xml')
exports('com.fasterxml.jackson.databind')
exports('com.fasterxml.jackson.databind.annotation')
exports('com.fasterxml.jackson.databind.cfg')
exports('com.fasterxml.jackson.databind.deser')
exports('com.fasterxml.jackson.databind.deser.impl')
exports('com.fasterxml.jackson.databind.deser.std')
exports('com.fasterxml.jackson.databind.exc')
exports('com.fasterxml.jackson.databind.ext')
exports('com.fasterxml.jackson.databind.introspect')
exports('com.fasterxml.jackson.databind.json')
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
exports('com.fasterxml.jackson.databind.jsonschema')
exports('com.fasterxml.jackson.databind.jsontype')
exports('com.fasterxml.jackson.databind.jsontype.impl')
exports('com.fasterxml.jackson.databind.module')
exports('com.fasterxml.jackson.databind.node')
exports('com.fasterxml.jackson.databind.ser')
exports('com.fasterxml.jackson.databind.ser.impl')
exports('com.fasterxml.jackson.databind.ser.std')
exports('com.fasterxml.jackson.databind.type')
exports('com.fasterxml.jackson.databind.util')
uses('com.fasterxml.jackson.databind.Module')
}
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
exports('com.csvreader')
}
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
exports('org.slf4j.bridge')
requires('java.logging')
requires('org.slf4j')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
}
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
exports('mu')
requires('kotlin.stdlib')
requires('org.slf4j')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
exports('com.google.common.eventbus')
exports('com.google.common.net')
exports('com.google.common.base')
exports('com.google.common.collect')
exports('com.google.common.io')
exports('com.google.common.primitives')
exports('com.google.common.math')
requires('failureaccess')
requires('java.logging')
}
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
exports('org.reactfx.util')
requires('javafx.base')
requires('javafx.graphics')
requires('javafx.controls')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
requires('javafx.base')
requires('javafx.graphics')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
}
module('hummingbird-1.6.2.jar', 'com.sparrowwallet.hummingbird', '1.6.2') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.17-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.17-SNAPSHOT') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
requires('org.bouncycastle.provider')
requires('com.fasterxml.jackson.databind')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('org.json')
exports('com.samourai.http.client')
exports('com.samourai.tor.client')
exports('com.samourai.wallet.api.backend')
exports('com.samourai.wallet.api.backend.beans')
exports('com.samourai.wallet.client.indexHandler')
exports('com.samourai.wallet.hd')
exports('com.samourai.wallet.util')
exports('com.samourai.whirlpool.client.event')
exports('com.samourai.whirlpool.client.wallet')
exports('com.samourai.whirlpool.client.wallet.beans')
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
exports('com.samourai.whirlpool.client.whirlpool')
exports('com.samourai.whirlpool.client.whirlpool.beans')
exports('com.samourai.whirlpool.client.wallet.data.pool')
exports('com.samourai.whirlpool.client.wallet.data.utxo')
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
exports('com.samourai.whirlpool.client.wallet.data.supplier')
exports('com.samourai.whirlpool.client.mix.handler')
exports('com.samourai.whirlpool.client.mix.listener')
exports('com.samourai.whirlpool.protocol.beans')
exports('com.samourai.whirlpool.protocol.rest')
exports('com.samourai.whirlpool.client.tx0')
exports('com.samourai.wallet.segwit.bech32')
exports('com.samourai.whirlpool.client.wallet.data.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState')
exports('com.sparrowwallet.nightjar.http')
exports('com.sparrowwallet.nightjar.stomp')
exports('com.sparrowwallet.nightjar.tor')
}
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
exports('com.zeroleak.throwingsupplier')
}
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
exports('com.squareup.okhttp')
}
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
exports('com.auth0.jwt')
}
module('json-20180130.jar', 'org.json', '1.0') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
exports('com.google.protobuf')
}
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
exports('org.apache.commons.text')
}
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
requires('kotlin.stdlib')
requires('commons.compress')
requires('org.tukaani.xz')
requires('java.management')
requires('io.github.microutils.kotlin.logging')
}
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
exports('net.freehaven.tor.control')
}
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
exports('org.apache.commons.compress')
requires('org.tukaani.xz')
}
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
exports('org.tukaani.xz')
}
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
exports('com.runjva.sourceforge.jsocks.protocol')
requires('org.slf4j')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
}
}
@@ -0,0 +1,54 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}
@@ -0,0 +1,52 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}
@@ -0,0 +1,174 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if (isModule(originalJar) && !originalJarName.contains("jackson")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
if(manifest == null) {
manifest = new Manifest();
}
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
}
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
for (String usesName : moduleInfo.getUses()) {
moduleVisitor.visitUse(usesName.replace('.', '/'));
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
@@ -0,0 +1,62 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
private List<String> uses = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public void uses(String uses) {
this.uses.add(uses);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
protected List<String> getUses() {
return uses;
}
}
+11
View File
@@ -0,0 +1,11 @@
## Sparrow Wallet Repository Docs
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
The documentation here is mainly developer-related resources.
### [Reproducible builds](reproducible.md)
Documentation to create and verify a build of the project against the released binaries.
+78
View File
@@ -0,0 +1,78 @@
## Reproducible builds
Reproducibility is a goal of the Sparrow Wallet project.
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
Work on resolving both of these issues is ongoing.
### Reproducing a release
#### Install Java
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
```shell
sudo apt-get install -y wget apt-transport-https gnupg
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
rm adoptopenjdk-keyring.gpg
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
sudo apt update -y
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
```
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 16.0.1.hs-adpt
```
#### Other requirements
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
```shell
sudo apt install -y rpm fakeroot binutils
```
#### Building the binaries
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.0"
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: 49654b7c82...c9e57fad01
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
+2
View File
@@ -0,0 +1,2 @@
mime-type=x-scheme-handler/aopp
description=Verify Address Ownership URI
-3
View File
@@ -1,3 +0,0 @@
extension=psbt
mime-type=application/octet-stream
description=Partially Signed Bitcoin Transaction
+2
View File
@@ -0,0 +1,2 @@
mime-type=x-scheme-handler/bitcoin
description=Bitcoin Scheme URI
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Sparrow
Comment=Sparrow
Exec=/opt/sparrow/bin/Sparrow %U
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
+84 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.3.2</string>
<string>1.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -35,5 +35,88 @@
<string>true</string>
<key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.sparrowwallet.sparrow.bitcoin</string>
<key>CFBundleURLSchemes</key>
<array>
<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>
<dict>
<key>UTTypeIdentifier</key>
<string>org.bitcoin.psbt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>psbt</string>
</array>
</dict>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>org.bitcoin.txn</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txn</string>
</array>
</dict>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeIconFile</key>
<string>sparrow.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>psbt</string>
</array>
<key>CFBundleTypeName</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeIconFile</key>
<string>sparrow.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>txn</string>
</array>
<key>CFBundleTypeName</key>
<string>Bitcoin Transaction</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>
</dict>
</plist>
+163
View File
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<?ifdef JpIsSystemWide ?>
<?define JpInstallScope="perMachine"?>
<?else?>
<?define JpInstallScope="perUser"?>
<?endif?>
<?define JpProductLanguage=1033 ?>
<?define JpInstallerVersion=200 ?>
<?define JpCompressedMsi=yes ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
<Product
Id="$(var.JpProductCode)"
Name="$(var.JpAppName)"
Language="$(var.JpProductLanguage)"
Version="$(var.JpAppVersion)"
Manufacturer="$(var.JpAppVendor)"
UpgradeCode="$(var.JpProductUpgradeCode)">
<Package
Description="$(var.JpAppDescription)"
Manufacturer="$(var.JpAppVendor)"
InstallerVersion="$(var.JpInstallerVersion)"
Compressed="$(var.JpCompressedMsi)"
InstallScope="$(var.JpInstallScope)" Platform="x64"
/>
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
<?ifdef JpAllowDowngrades ?>
<?ifdef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="yes"
Disallow="no"
/>
<?endif?>
<?endif?>
<?ifdef JpAllowDowngrades ?>
<?ifndef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="yes"
Disallow="yes"
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
/>
<?endif?>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<?ifdef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="no"
Disallow="no"
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
/>
<?endif?>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<?ifndef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="no"
Disallow="yes"
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
/>
<?endif?>
<?endif?>
<!-- Standard required root -->
<Directory Id="TARGETDIR" Name="SourceDir"/>
<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"/>
<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>
</Component>
</DirectoryRef>
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
<ComponentGroupRef Id="Shortcuts"/>
<ComponentGroupRef Id="Files"/>
<ComponentGroupRef Id="FileAssociations"/>
<ComponentRef Id="RegistryEntries"/>
</Feature>
<?ifdef JpInstallDirChooser ?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
<?endif?>
<UI>
<?ifdef JpInstallDirChooser ?>
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
</Control>
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
</Control>
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
<Text>!(loc.message.install.dir.exist)</Text>
</Control>
</Dialog>
<!--
Run WixUI_InstallDir dialog in the default install directory.
-->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
<UIRef Id="WixUI_InstallDir" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
<?ifndef JpLicenseRtf ?>
<!--
No license file provided.
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
to exclude license dialog.
-->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
<?endif?>
<?else?>
<?ifdef JpLicenseRtf ?>
<UIRef Id="WixUI_Minimal" />
<?endif?>
<?endif?>
</UI>
<?ifdef JpLicenseRtf ?>
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
<?endif?>
</Product>
</Wix>
+3
View File
@@ -0,0 +1,3 @@
mime-type=application/psbt
extension=psbt
description=Partially Signed Bitcoin Transaction
+3
View File
@@ -0,0 +1,3 @@
mime-type=application/bitcoin-transaction
extension=txn
description=Bitcoin Transaction
@@ -12,7 +12,7 @@ public class AboutController {
private Label title;
public void initializeView() {
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
}
public void setStage(Stage stage) {
File diff suppressed because it is too large Load Diff
@@ -2,20 +2,22 @@ package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.BlockHeader;
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.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.TextUtils;
import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -27,25 +29,35 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.*;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
import org.berndpruenster.netlayer.tor.Tor;
import org.controlsfx.control.HyperlinkLabel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenURIHandler;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class AppServices {
@@ -58,9 +70,12 @@ public class AppServices {
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
private static AppServices INSTANCE;
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
private final MainApp application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
@@ -81,6 +96,8 @@ public class AppServices {
private static Integer currentBlockHeight;
private static BlockHeader latestBlockHeader;
private static Map<Integer, Double> targetBlockFeeRates;
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
@@ -91,6 +108,10 @@ public class AppServices {
private static List<Device> devices;
private static final List<File> argFiles = new ArrayList<>();
private static final List<URI> argUris = new ArrayList<>();
private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
@@ -98,6 +119,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();
@@ -110,9 +136,18 @@ public class AppServices {
}
};
private static final OpenURIHandler openURIHandler = event -> {
openURI(event.getURI());
};
private static final OpenFilesHandler openFilesHandler = event -> {
openFiles(event.getFiles(), null);
};
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
}
public void start() {
@@ -131,6 +166,8 @@ public class AppServices {
restartServices();
}
}
addURIHandlers();
}
private void restartServices() {
@@ -216,7 +253,7 @@ public class AppServices {
connectionService.setRestartOnFailure(false);
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
File crtFile = Config.get().getElectrumServerCert();
if(crtFile != null) {
if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nChange the configured server certificate if you would like to proceed.");
@@ -247,8 +284,7 @@ public class AppServices {
onlineProperty.addListener(onlineServicesListener);
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
List<String> otherServers = Arrays.stream(PublicElectrumServer.values()).map(PublicElectrumServer::getUrl).filter(url -> !url.equals(Config.get().getPublicElectrumServer())).collect(Collectors.toList());
Config.get().setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
Config.get().changePublicServer();
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
}
@@ -350,18 +386,36 @@ public class AppServices {
return Tor.getDefault() != null;
}
public static boolean isUsingProxy() {
return isTorRunning() || Config.get().isUseProxy();
}
public static Proxy getProxy() {
return getProxy(TOR_DEFAULT_PROXY_CIRCUIT_ID);
}
public static Proxy getProxy(String proxyCircuitId) {
Config config = Config.get();
Proxy proxy = null;
if(config.isUseProxy()) {
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
} else if(AppServices.isTorRunning()) {
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
}
return null;
//Setting new proxy authentication credentials will force a new Tor circuit to be created
if(proxy != null) {
Authenticator.setDefault(new Authenticator() {
public PasswordAuthentication getPasswordAuthentication() {
return (new PasswordAuthentication("user", proxyCircuitId.toCharArray()));
}
});
}
return proxy;
}
static void initialize(MainApp application) {
@@ -372,6 +426,10 @@ public class AppServices {
return INSTANCE;
}
public static WhirlpoolServices getWhirlpoolServices() {
return get().whirlpoolServices;
}
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
@@ -413,6 +471,14 @@ public class AppServices {
stage.hide();
}
public static void onEscapePressed(Scene scene, Runnable runnable) {
scene.setOnKeyPressed(event -> {
if(event.getCode() == KeyCode.ESCAPE) {
runnable.run();
}
});
}
public Map<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
@@ -424,8 +490,12 @@ public class AppServices {
return openWallets;
}
public Window getWindowForWallet(Storage storage) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
public Wallet getWallet(String walletId) {
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
}
public Window getWindowForWallet(String walletId) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
}
@@ -454,6 +524,10 @@ public class AppServices {
return currentBlockHeight;
}
public static BlockHeader getLatestBlockHeader() {
return latestBlockHeader;
}
public static Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
@@ -494,6 +568,14 @@ public class AppServices {
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
}
public static boolean isWalletFile(File file) {
return Storage.isWalletFile(file);
}
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
@@ -508,6 +590,30 @@ public class AppServices {
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
Matcher matcher = linkPattern.matcher(content);
if(matcher.find()) {
String link = matcher.group(1);
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
hyperlinkLabel.getStyleClass().add("content");
Label label = new Label();
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
hyperlinkLabel.setOnAction(event -> {
alert.close();
get().getApplication().getHostServices().showDocument(link);
});
alert.getDialogPane().setContent(hyperlinkLabel);
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3) {
alert.getDialogPane().setPrefHeight(200 + lines.length * 20);
}
moveToActiveWindowScreen(alert);
return alert.showAndWait();
}
@@ -520,6 +626,171 @@ public class AppServices {
}
}
public static Window getActiveWindow() {
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : null);
}
public static void moveToActiveWindowScreen(Dialog<?> dialog) {
Window activeWindow = getActiveWindow();
if(activeWindow != null) {
moveToWindowScreen(activeWindow, dialog);
}
}
public static void moveToActiveWindowScreen(Window newWindow, double newWindowWidth, double newWindowHeight) {
Window activeWindow = getActiveWindow();
if(activeWindow != null) {
moveToWindowScreen(activeWindow, newWindow, newWindowWidth, newWindowHeight);
}
}
public static void moveToWindowScreen(Window currentWindow, Dialog<?> dialog) {
Window newWindow = dialog.getDialogPane().getScene().getWindow();
DialogPane dialogPane = dialog.getDialogPane();
double dialogWidth = dialogPane.getPrefWidth() > 0.0 ? dialogPane.getPrefWidth() : (dialogPane.getWidth() > 0.0 ? dialogPane.getWidth() : 360);
double dialogHeight = dialogPane.getPrefHeight() > 0.0 ? dialogPane.getPrefHeight() : (dialogPane.getHeight() > 0.0 ? dialogPane.getHeight() : 200);
moveToWindowScreen(currentWindow, newWindow, dialogWidth, dialogHeight);
}
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())) {
double x = currentWindow.getX() + (currentWindow.getWidth() / 2) - (newWindowWidth / 2);
double y = currentWindow.getY() + (currentWindow.getHeight() / 2.2) - (newWindowHeight / 2);
newWindow.setX(x);
newWindow.setY(y);
}
}
static void parseFileUriArguments(List<String> fileUriArguments) {
for(String fileUri : fileUriArguments) {
try {
File file = new File(fileUri.replace("~", System.getProperty("user.home")));
if(file.exists()) {
argFiles.add(file);
continue;
}
URI uri = new URI(fileUri);
argUris.add(uri);
} catch(URISyntaxException e) {
log.warn("Could not parse " + fileUri + " as a valid file or URI");
} catch(Exception e) {
//ignore
}
}
}
public static void openFileUriArguments(Window window) {
openFiles(argFiles, window);
argFiles.clear();
for(URI argUri : argUris) {
openURI(argUri);
}
argUris.clear();
}
private static void openFiles(List<File> files, Window window) {
final List<File> openFiles = new ArrayList<>(files);
Platform.runLater(() -> {
Window openWindow = window;
if(openWindow == null) {
openWindow = getActiveWindow();
}
if(openWindow instanceof Stage) {
((Stage)openWindow).setAlwaysOnTop(true);
((Stage)openWindow).setAlwaysOnTop(false);
}
for(File file : openFiles) {
if(isWalletFile(file)) {
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
} else {
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
}
}
});
}
private static void openURI(URI uri) {
Platform.runLater(() -> {
if("bitcoin".equals(uri.getScheme())) {
openBitcoinUri(uri);
} else if("aopp".equals(uri.getScheme())) {
openAddressOwnershipProof(uri);
}
});
}
public static void addURIHandlers() {
try {
if(Desktop.isDesktopSupported()) {
if(Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
Desktop.getDesktop().setOpenFileHandler(openFilesHandler);
}
if(Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_URI)) {
Desktop.getDesktop().setOpenURIHandler(openURIHandler);
}
}
} catch(Exception e) {
log.error("Could not add URI handler", e);
}
}
private static void openBitcoinUri(URI uri) {
try {
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
Wallet wallet = selectWallet(null, "pay from");
if(wallet != null) {
final Wallet sendingWallet = wallet;
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet())));
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
}
} catch(Exception e) {
showErrorDialog("Not a valid bitcoin URI", e.getMessage());
}
}
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());
if(wallets.isEmpty()) {
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
} else if(wallets.size() == 1) {
wallet = wallets.iterator().next();
} else {
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
walletChoiceDialog.setTitle("Choose Wallet");
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
Image image = new Image("/image/sparrow-small.png");
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
moveToActiveWindowScreen(walletChoiceDialog);
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
if(optWallet.isPresent()) {
wallet = optWallet.get();
}
}
return wallet;
}
public static Font getMonospaceFont() {
return Font.font("Roboto Mono", 13);
}
@@ -527,9 +798,11 @@ public class AppServices {
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader();
}
@Subscribe
@@ -540,6 +813,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));
}
@@ -590,7 +865,10 @@ public class AppServices {
@Subscribe
public void openWallets(OpenWalletsEvent event) {
if(event.getWalletTabDataList().isEmpty()) {
walletWindows.remove(event.getWindow());
List<WalletTabData> closedTabData = walletWindows.remove(event.getWindow());
if(closedTabData != null && !closedTabData.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedTabData));
}
} else {
walletWindows.put(event.getWindow(), event.getWalletTabDataList());
}
@@ -599,7 +877,7 @@ public class AppServices {
Platform.runLater(() -> {
if(!Window.getWindows().isEmpty()) {
List<File> walletFiles = allWallets.stream().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());
}
});
@@ -667,4 +945,14 @@ public class AppServices {
connectionService.start();
}
}
@Subscribe
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
onlineProperty.set(false);
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
}
}
@@ -7,14 +7,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.FileType;
import com.sparrowwallet.sparrow.io.IOUtils;
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;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.instance.InstanceException;
import com.sparrowwallet.sparrow.instance.InstanceList;
import javafx.application.Application;
import javafx.scene.text.Font;
import javafx.stage.Stage;
@@ -29,16 +29,26 @@ import java.util.*;
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.3.2";
public static final String APP_VERSION = "1.5.0";
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";
private Stage mainStage;
private static SparrowInstance sparrowInstance;
@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();
}
@@ -86,6 +96,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) {
@@ -94,9 +109,8 @@ public class MainApp extends Application {
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList());
Collections.reverse(encryptedWalletFiles);
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
@@ -108,13 +122,20 @@ public class MainApp extends Application {
}
}
AppServices.openFileUriArguments(stage);
AppServices.get().start();
}
@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();
}
}
public static void main(String[] argv) {
@@ -162,6 +183,19 @@ public class MainApp extends Application {
getLogger().info("Using " + Network.get() + " configuration");
}
List<String> fileUriArguments = jCommander.getUnknownOptions();
try {
sparrowInstance = new SparrowInstance(fileUriArguments);
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
} catch(InstanceException e) {
getLogger().error("Could not access application lock", e);
}
if(!fileUriArguments.isEmpty()) {
AppServices.parseFileUriArguments(fileUriArguments);
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
@@ -170,4 +204,31 @@ public class MainApp extends Application {
private static Logger getLogger() {
return LoggerFactory.getLogger(MainApp.class);
}
private static class SparrowInstance extends InstanceList {
private final List<String> fileUriArguments;
public SparrowInstance(List<String> fileUriArguments) {
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
this.fileUriArguments = fileUriArguments;
}
@Override
protected void receiveMessageList(List<String> messageList) {
if(messageList != null && !messageList.isEmpty()) {
AppServices.parseFileUriArguments(messageList);
AppServices.openFileUriArguments(null);
}
}
@Override
protected List<String> sendMessageList() {
return fileUriArguments;
}
@Override
protected void beforeExit() {
getLogger().info("Opening files/URIs in already running instance, exiting...");
}
}
}
@@ -7,7 +7,10 @@ import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
@@ -48,6 +51,10 @@ public class WelcomeController {
welcomeBox.getStyleClass().add("offline");
serverStatus.setText("Offline");
serverToggle.addEventFilter(MouseEvent.MOUSE_RELEASED, Event::consume);
Tooltip tooltip = new Tooltip("Demonstration only - you are not connected!");
tooltip.setShowDelay(Duration.ZERO);
serverToggle.setTooltip(tooltip);
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
});
@@ -12,6 +12,7 @@ public class WelcomeDialog extends Dialog<Mode> {
public WelcomeDialog() {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
try {
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
@@ -21,6 +22,7 @@ public class WelcomeDialog extends Dialog<Mode> {
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
@@ -10,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(), null));
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
if(utxoEntry.isDuplicateAddress()) {
if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
setGraphic(getDuplicateGlyph());
Tooltip tt = new Tooltip();
tt.setText(getTooltipText(utxoEntry));
setTooltip(tt);
} else {
setGraphic(null);
}
});
}
}
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -0,0 +1,37 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import javafx.util.StringConverter;
public class AddressStringConverter extends StringConverter<Address> {
@Override
public Address fromString(String value) {
// If the specified value is null or zero-length, return null
if(value == null) {
return null;
}
value = value.trim();
if (value.length() < 1) {
return null;
}
try {
return Address.fromString(value);
} catch(InvalidAddressException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public String toString(Address value) {
// If the specified value is null, return a zero-length String
if(value == null) {
return "";
}
return value.toString();
}
}
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
@@ -33,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));
}
@@ -82,6 +83,10 @@ public class AddressTreeTable extends CoinTreeTable {
}
}
});
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
});
}
public void updateAll(NodeEntry rootEntry) {
@@ -99,22 +104,21 @@ public class AddressTreeTable extends CoinTreeTable {
}
public void updateHistory(List<WalletNode> updatedNodes) {
//We only ever add or replace child nodes - never remove in order to keep a full sequence
//We only ever add child nodes - never remove in order to keep a full sequence
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
for(WalletNode updatedNode : updatedNodes) {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
if(optEntry.isPresent()) {
int index = rootEntry.getChildren().indexOf(optEntry.get());
rootEntry.getChildren().set(index, nodeEntry);
NodeEntry existingEntry = (NodeEntry)optEntry.get();
existingEntry.refreshChildren();
} else {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
rootEntry.getChildren().add(nodeEntry);
}
}
sort();
refresh();
}
public void updateLabel(Entry entry) {
@@ -10,16 +10,24 @@ import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.scene.layout.Region;
import org.controlsfx.tools.Platform;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
class CoinCell extends TreeTableCell<Entry, Number> {
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final Tooltip tooltip;
public CoinCell() {
super();
tooltip = new Tooltip();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
}
}
@Override
@@ -38,7 +46,8 @@ class CoinCell extends TreeTableCell<Entry, Number> {
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
@@ -66,6 +75,10 @@ class CoinCell extends TreeTableCell<Entry, Number> {
} else {
setGraphic(null);
}
if(amount.longValue() < 0) {
getStyleClass().add("negative-amount");
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry) {
@@ -5,8 +5,8 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
@@ -92,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
}
});
if(wallet.getBirthDate() == null) {
@@ -34,7 +34,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
} else {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
@@ -25,8 +25,8 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, outputDescriptor, device);
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(wallet, outputDescriptor, device, defaultDevice);
}
@Subscribe
@@ -22,6 +22,7 @@ import java.util.Objects;
public abstract class DeviceDialog<R> extends Dialog<R> {
private final List<String> operationFingerprints;
private final Accordion deviceAccordion;
private final Button scanButton;
private final VBox scanBox;
private final Label scanLabel;
@@ -57,18 +58,19 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
usb.setFontSize(50);
scanLabel = new Label("Connect Hardware Wallet");
Button button = new Button("Scan...");
button.setPrefSize(120, 60);
button.setOnAction(event -> {
scanButton = new Button("Scan...");
scanButton.setPrefSize(120, 60);
scanButton.setOnAction(event -> {
scan();
});
scanBox.getChildren().addAll(usb, scanLabel, button);
scanBox.getChildren().addAll(usb, scanLabel, scanButton);
scanBox.managedProperty().bind(scanBox.visibleProperty());
stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppServices.getDevices();
if(devices == null || devices.isEmpty()) {
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
} else {
Platform.runLater(() -> setDevices(devices));
@@ -89,6 +91,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
}
@@ -96,15 +99,21 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
scanButton.setText("Scan...");
List<Device> devices = enumerateService.getValue();
setDevices(devices);
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
scanButton.setText("Scan...");
deviceAccordion.getPanes().clear();
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
enumerateService.setOnRunning(workerStateEvent -> {
scanButton.setText("Scanning...");
});
enumerateService.start();
}
@@ -114,23 +123,24 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
if(operationFingerprints != null) {
dialogDevices.removeIf(device -> {
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.getNeedsPinSent() || device.getNeedsPassphraseSent());
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.isNeedsPinSent() || device.isNeedsPassphraseSent());
});
}
deviceAccordion.getPanes().clear();
if(dialogDevices.isEmpty()) {
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
scanLabel.setText("No matching devices found");
} else {
scanBox.setVisible(false);
for(Device device : dialogDevices) {
DevicePane devicePane = getDevicePane(device);
DevicePane devicePane = getDevicePane(device, dialogDevices.size() == 1);
deviceAccordion.getPanes().add(devicePane);
}
}
}
protected abstract DevicePane getDevicePane(Device device);
protected abstract DevicePane getDevicePane(Device device, boolean defaultDevice);
}
@@ -59,7 +59,9 @@ public class DevicePane extends TitledDescriptionPane {
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
public DevicePane(Wallet wallet, Device device) {
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
@@ -68,6 +70,7 @@ public class DevicePane extends TitledDescriptionPane {
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
@@ -80,7 +83,7 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
public DevicePane(PSBT psbt, Device device) {
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
@@ -89,6 +92,7 @@ public class DevicePane extends TitledDescriptionPane {
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
@@ -101,7 +105,7 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
@@ -110,6 +114,7 @@ public class DevicePane extends TitledDescriptionPane {
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
@@ -122,7 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
@@ -131,6 +136,7 @@ public class DevicePane extends TitledDescriptionPane {
this.keyDerivation = keyDerivation;
this.message = message;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
@@ -144,9 +150,10 @@ public class DevicePane extends TitledDescriptionPane {
}
private void initialise(Device device) {
if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
} else if(device.isNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else if(device.getError() != null) {
setError("Error", device.getError());
@@ -165,7 +172,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private void setDefaultStatus() {
setDescription(device.getNeedsPinSent() ? "Locked" : device.getNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
}
private void createUnlockButton() {
@@ -229,7 +236,6 @@ public class DevicePane extends TitledDescriptionPane {
private void createSignButton() {
signButton = new Button("Sign");
signButton.setDefaultButton(true);
signButton.setAlignment(Pos.CENTER_RIGHT);
signButton.setMinWidth(44);
signButton.setOnAction(event -> {
@@ -242,7 +248,6 @@ public class DevicePane extends TitledDescriptionPane {
private void createDisplayAddressButton() {
displayAddressButton = new Button("Display Address");
displayAddressButton.setDefaultButton(true);
displayAddressButton.setAlignment(Pos.CENTER_RIGHT);
displayAddressButton.setOnAction(event -> {
displayAddressButton.setDisable(true);
@@ -259,7 +264,6 @@ public class DevicePane extends TitledDescriptionPane {
private void createSignMessageButton() {
signMessageButton = new Button("Sign Message");
signMessageButton.setDefaultButton(true);
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
signMessageButton.setOnAction(event -> {
signMessageButton.setDisable(true);
@@ -284,7 +288,9 @@ public class DevicePane extends TitledDescriptionPane {
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
Platform.runLater(() -> pinField.requestFocus());
enterPinButton = new Button("Enter PIN");
enterPinButton.setDefaultButton(true);
enterPinButton.setOnAction(event -> {
enterPinButton.setDisable(true);
sendPin(pinField.getText());
@@ -324,9 +330,15 @@ public class DevicePane extends TitledDescriptionPane {
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
passphraseField.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
sendPassphrase(passphrase.get());
});
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
sendPassphraseButton.setText("Send Passphrase");
sendPassphraseButton.getStyleClass().add("default-button");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
@@ -411,7 +423,7 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(false);
unlockButton.setVisible(false);
if(device.getNeedsPassphraseSent()) {
if(device.isNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
@@ -534,7 +546,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);
@@ -604,17 +616,23 @@ public class DevicePane extends TitledDescriptionPane {
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
importButton.getStyleClass().add("default-button");
}
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setDefaultButton(defaultDevice);
displayAddressButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
signMessageButton.setDefaultButton(defaultDevice);
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
}
@@ -627,11 +645,11 @@ public class DevicePane extends TitledDescriptionPane {
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
Button importDerivationButton = new Button("Import");
importDerivationButton.setOnAction(event -> {
@@ -22,8 +22,8 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(psbt, device);
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(psbt, device, defaultDevice);
}
@Subscribe
@@ -26,8 +26,8 @@ public class DeviceSignMessageDialog extends DeviceDialog<String> {
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, message, keyDerivation, device);
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(wallet, message, keyDerivation, device, defaultDevice);
}
@Subscribe
@@ -3,7 +3,9 @@ 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;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@@ -72,9 +74,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
HBox actionBox = new HBox();
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
});
@@ -83,21 +83,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
increaseFeeGlyph.setFontSize(12);
increaseFeeButton.setGraphic(increaseFeeGlyph);
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
increaseFeeButton.setOnAction(event -> {
increaseFee(transactionEntry);
});
actionBox.getChildren().add(increaseFeeButton);
}
if(blockTransaction.getHeight() <= 0 && containsWalletOutputs(transactionEntry)) {
Button cpfpButton = new Button("");
cpfpButton.setGraphic(getIncreaseFeeCPFPGlyph());
cpfpButton.setOnAction(event -> {
createCpfp(transactionEntry);
});
actionBox.getChildren().add(cpfpButton);
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
setTooltip(tooltip);
@@ -105,30 +112,30 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
HBox actionBox = new HBox();
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
receiveButton.setGraphic(receiveGlyph);
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
actionBox.getChildren().add(receiveButton);
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
(nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
if(canSignMessage(nodeEntry.getWallet())) {
Button signMessageButton = new Button("");
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12);
signMessageButton.setGraphic(signMessageGlyph);
signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> {
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
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());
@@ -139,9 +146,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
HBox actionBox = new HBox();
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
@@ -149,9 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
Button spendUtxoButton = new Button("");
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.setFontSize(12);
spendUtxoButton.setGraphic(sendGlyph);
spendUtxoButton.setGraphic(getSendGlyph());
spendUtxoButton.setOnAction(event -> {
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
});
@@ -171,6 +174,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.filter(TransactionInput::isReplaceByFeeEnabled)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
@@ -238,13 +242,49 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
private static Double getMaxFeeRate() {
if(AppServices.getTargetBlockFeeRates().isEmpty()) {
if(AppServices.getTargetBlockFeeRates() == null || AppServices.getTargetBlockFeeRates().isEmpty()) {
return 100.0;
}
return AppServices.getTargetBlockFeeRates().values().iterator().next();
}
private static void createCpfp(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.map(HashIndexEntry::getHashIndex)
.collect(Collectors.toList());
if(ourOutputs.isEmpty()) {
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
}
BlockTransactionHashIndex utxo = ourOutputs.get(0);
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(Wallet wallet) {
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)
.map(e -> (HashIndexEntry)e)
.anyMatch(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT));
}
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
@@ -262,19 +302,117 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
}
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() != Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(null);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() == Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(null));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static Glyph getViewTransactionGlyph() {
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
return searchGlyph;
}
private static Glyph getIncreaseFeeRBFGlyph() {
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
increaseFeeGlyph.setFontSize(12);
return increaseFeeGlyph;
}
private static Glyph getIncreaseFeeCPFPGlyph() {
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
cpfpGlyph.setFontSize(12);
return cpfpGlyph;
}
private static Glyph getReceiveGlyph() {
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
return receiveGlyph;
}
private static Glyph getSignMessageGlyph() {
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12);
return signMessageGlyph;
}
private static Glyph getSendGlyph() {
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.setFontSize(12);
return sendGlyph;
}
private static Glyph getCopyGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COPY);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getFreezeGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SNOWFLAKE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getUnfreezeGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SUN);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
getItems().add(viewTransaction);
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
hide();
increaseFee(transactionEntry);
});
getItems().add(increaseFee);
}
if(containsWalletOutputs(transactionEntry)) {
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
createCpfp.setOnAction(AE -> {
hide();
createCpfp(transactionEntry);
});
getItems().add(createCpfp);
}
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
@@ -284,21 +422,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().add(copyTxid);
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee");
increaseFee.setOnAction(AE -> {
hide();
increaseFee(transactionEntry);
});
getItems().add(increaseFee);
}
}
}
private static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
@@ -323,12 +458,32 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyDate, copyTxid, copyHeight);
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
}
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
hide();
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
getItems().add(receiveToAddress);
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
hide();
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
@@ -354,34 +509,22 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
if(nodeEntry != null) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setOnAction(AE -> {
hide();
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
}
}
private static class HashIndexEntryContextMenu extends ContextMenu {
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), hashIndexEntry.getBlockTransaction()));
});
getItems().add(copyHashIndex);
getItems().add(viewTransaction);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
MenuItem sendSelected = new MenuItem("Send Selected");
sendSelected.setGraphic(getSendGlyph());
sendSelected.setOnAction(AE -> {
hide();
sendSelectedUtxos(treeTableView, hashIndexEntry);
@@ -392,20 +535,32 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.getHashIndex().isSpent()) {
if(hashIndexEntry.getHashIndex().getStatus() == null || hashIndexEntry.getHashIndex().getStatus() != Status.FROZEN) {
MenuItem freezeUtxo = new MenuItem("Freeze UTXO");
freezeUtxo.setGraphic(getFreezeGlyph());
freezeUtxo.setOnAction(AE -> {
hide();
freezeUtxo(hashIndexEntry);
freezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(freezeUtxo);
} else {
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
unfreezeUtxo.setOnAction(AE -> {
hide();
unfreezeUtxo(hashIndexEntry);
unfreezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(unfreezeUtxo);
}
}
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
}
}
@@ -416,6 +571,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming");
cell.getStyleClass().remove("negative-amount");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
@@ -4,7 +4,9 @@ import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport;
import com.sparrowwallet.sparrow.io.ImportException;
@@ -95,6 +97,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
new FileChooser.ExtensionFilter("TXT", "*.txt")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
importFile(file, null);
@@ -111,8 +114,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
importButton.setDisable(true);
setExpanded(true);
} else {
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
importFile(file.getName(), inputStream, password);
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
importFile(file.getName(), inputStream, password);
};
}
} catch (Exception e) {
log.error("Error importing file", e);
@@ -169,11 +173,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
if(wallets != null) {
for(Wallet wallet : wallets) {
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
return wallet.getKeystores().get(0);
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(importer.getName().replace(" Multisig", ""));
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(importer.getWalletModel());
return keystore;
}
}
throw new ImportException("Script type " + scriptType + " is not supported");
throw new ImportException("Script type " + scriptType.getDescription() + " is not supported in this QR. Check you are displaying the correct QR code.");
}
return null;
@@ -9,10 +9,7 @@ import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CoboVaultMultisig;
import com.sparrowwallet.sparrow.io.PassportMultisig;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.WalletExport;
import com.sparrowwallet.sparrow.io.*;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
@@ -83,10 +80,13 @@ 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.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);
if(file != null) {
exportWallet(file);
@@ -96,13 +96,13 @@ 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 File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
try {
@@ -112,10 +112,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
}
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
setError("Export Error", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet..."));
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
@@ -126,16 +126,17 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file, Wallet exportWallet) {
try {
if(file != null) {
OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(exportWallet, outputStream);
EventManager.get().post(new WalletExportEvent(exportWallet));
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(exportWallet, outputStream);
EventManager.get().post(new WalletExportEvent(exportWallet));
}
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(exportWallet, outputStream);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig) {
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
@@ -27,6 +28,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
@@ -42,35 +45,62 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
}
} else {
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
}
}
setContent(getScriptTypeEntry());
setContent(getScriptTypeEntry(scriptTypes));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Wallet wallet = new Wallet();
wallet.setName(fileName);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry() {
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
@@ -2,13 +2,13 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
@@ -19,6 +19,7 @@ public class HelpLabel extends Label {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
getStyleClass().add("help-label");
@@ -26,10 +27,10 @@ public class HelpLabel extends Label {
}
private static Glyph getHelpGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
lockGlyph.getStyleClass().add("help-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
glyph.getStyleClass().add("help-icon");
glyph.setFontSize(11);
return glyph;
}
public final StringProperty helpTextProperty() {
@@ -49,4 +50,18 @@ public class HelpLabel extends Label {
public final String getHelpText() {
return helpText == null ? "" : helpText.getValue();
}
public ObjectProperty<Node> helpGraphicProperty() {
if(helpGraphicProperty == null) {
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
}
return helpGraphicProperty;
}
private ObjectProperty<Node> helpGraphicProperty;
public final void setHelpGraphic(Node graphic) {
helpGraphicProperty().setValue(graphic);
}
}
@@ -30,6 +30,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
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.KEY);
key.setFontSize(50);
@@ -6,6 +6,7 @@ import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.util.converter.DefaultStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,10 +29,11 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
setText(label);
setContextMenu(new LabelContextMenu(label));
setContextMenu(new LabelContextMenu(entry, label));
}
}
@@ -80,7 +82,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
@@ -88,8 +90,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
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);
}
}
}
}
@@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
public class MasterKeyDisplayDialog extends Dialog<Void> {
public MasterKeyDisplayDialog(Keystore decryptedKeystore) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.getStyleClass().add("edge-to-edge");
scrollPane.setPrefHeight(200);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
Accordion keystoreAccordion = new Accordion();
scrollPane.setContent(keystoreAccordion);
XprvKeystoreImportPane keystorePane = new XprvKeystoreImportPane(decryptedKeystore);
keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane);
stackPane.getChildren().addAll(anchorPane);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(260);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
}
}
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
public static final int MAX_PERIOD_HOURS = 2;
private static final double Y_VALUE_BREAK_MVB = 3.0;
private Tooltip tooltip;
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
categoryAxis.setTickLabelRotation(0);
NumberAxis numberAxis = (NumberAxis)getYAxis();
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
return (vSizeBytes / (1000 * 1000)) + " MvB";
}
@Override
public Number fromString(String string) {
return null;
}
});
this.setOnMouseMoved(mouseEvent -> {
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
previousFeeRate = feeRate;
}
final double maxMvB = getMaxMvB(getData());
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
if(maxMvB > Y_VALUE_BREAK_MVB) {
return (vSizeBytes / (1000 * 1000)) + " MvB";
} else {
return (vSizeBytes / (1000)) + " kvB";
}
}
@Override
public Number fromString(String string) {
return null;
}
});
if(categories.keySet().iterator().hasNext()) {
String time = categories.values().iterator().next();
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
return categories;
}
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
double maxTotal = 0d;
for(Series<String, Number> series : seriesList) {
maxTotal = Math.max(maxTotal, getMaxMvB(series));
}
return maxTotal;
}
private static double getMaxMvB(Series<String, Number> series) {
double total = 0d;
for(XYChart.Data<String, Number> data : series.getData()) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
total += mvb;
}
return total;
}
private static class ChartTooltip extends VBox {
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
Label title = new Label("At " + time);
HBox titleBox = new HBox(title);
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
getChildren().add(titleBox);
double maxMvB = getMaxMvB(seriesList);
for(int i = seriesList.size() - 1; i >= 0; i--) {
Series<String, Number> series = seriesList.get(i);
for(XYChart.Data<String, Number> data : series.getData()) {
if(data.getXValue().equals(category)) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
if(mvb >= 0.01) {
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
double kvb = data.getYValue().doubleValue() / 1000;
double mvb = kvb / 1000;
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
Label label = new Label(series.getName() + ": " + amount);
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
@@ -24,6 +24,7 @@ import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
@@ -34,6 +35,7 @@ import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -43,8 +45,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final ToggleGroup formatGroup;
private final ToggleButton formatTrezor;
private final ToggleButton formatElectrum;
private final Wallet wallet;
private WalletNode walletNode;
private boolean electrumSignatureFormat;
private boolean closed;
/**
* Verification only constructor
@@ -69,11 +76,24 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
* @param walletNode Wallet node to derive address from
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
this(wallet, walletNode, null, null);
}
/**
* Sign and verify with preset address, and supplied title, message and dialog buttons
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
* @param title Header text of dialog
* @param msg Message to sign (all fields will be made uneditable)
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
if(wallet != null) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
}
@@ -84,7 +104,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
Image image = new Image("image/seed.png", 50, 50, false, false);
if (!image.isError()) {
@@ -129,51 +149,90 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
signature.setWrapText(true);
signatureField.getInputs().add(signature);
fieldset.getChildren().addAll(addressField, messageField, signatureField);
Field formatField = new Field();
formatField.setText("Format:");
formatGroup = new ToggleGroup();
formatElectrum = new ToggleButton("Standard (Electrum)");
formatTrezor = new ToggleButton("BIP137 (Trezor)");
SegmentedButton formatButtons = new SegmentedButton(formatElectrum, formatTrezor);
formatButtons.setToggleGroup(formatGroup);
formatField.getInputs().add(formatButtons);
formatGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
electrumSignatureFormat = (newValue == formatElectrum);
});
formatButtons.setDisable(wallet != null && walletNode != null && wallet.getScriptType() == ScriptType.P2PKH);
fieldset.getChildren().addAll(addressField, messageField, signatureField, formatField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
if(msg != null) {
message.setText(msg);
address.setEditable(false);
message.setEditable(false);
signature.setEditable(false);
formatButtons.setDisable(true);
}
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
signButton.setDisable(wallet == null);
signButton.setOnAction(event -> {
signMessage();
});
if(buttons.length > 0) {
dialogPane.getButtonTypes().addAll(buttons);
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
verifyButton.setDefaultButton(false);
verifyButton.setOnAction(event -> {
verifyMessage();
});
boolean validAddress = isValidAddress();
signButton.setDisable(!validAddress || (wallet == null));
verifyButton.setDisable(!validAddress);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = isValidAddress();
signButton.setDisable(!valid || (wallet == null));
verifyButton.setDisable(!valid);
if(valid && wallet != null) {
try {
Address address = getAddress();
setWalletNodeFromAddress(wallet, address);
} catch(InvalidAddressException e) {
//can't happen
}
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
if(customSignButtonType != null) {
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
customSignButton.setDefaultButton(true);
customSignButton.setOnAction(event -> {
customSignButton.setDisable(true);
signMessage();
setResult(ButtonBar.ButtonData.OK_DONE);
});
}
});
} else {
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
signButton.setDisable(wallet == null);
signButton.setOnAction(event -> {
signMessage();
});
Button verifyButton = (Button) dialogPane.lookupButton(verifyButtonType);
verifyButton.setDefaultButton(false);
verifyButton.setOnAction(event -> {
verifyMessage();
});
boolean validAddress = isValidAddress();
signButton.setDisable(!validAddress || (wallet == null));
verifyButton.setDisable(!validAddress);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = isValidAddress();
signButton.setDisable(!valid || (wallet == null));
verifyButton.setDisable(!valid);
if(valid && wallet != null) {
try {
Address address = getAddress();
setWalletNodeFromAddress(wallet, address);
} catch(InvalidAddressException e) {
//can't happen
}
}
});
}
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -182,20 +241,49 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
EventManager.get().unregister(this);
if(!closed) {
EventManager.get().unregister(this);
closed = true;
}
});
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
Platform.runLater(() -> {
if(address.getText().isEmpty()) {
address.requestFocus();
} else if(message.getText().isEmpty()) {
message.requestFocus();
}
formatGroup.selectToggle(formatElectrum);
});
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
public String getSignature() {
return signature.getText();
}
/**
* Use the Electrum signing format, which uses the non-segwit compressed signing parameters for both segwit types (p2sh-p2wpkh and p2wpkh)
*
* @param electrumSignatureFormat
*/
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
formatGroup.selectToggle(electrumSignatureFormat ? formatElectrum : formatTrezor);
this.electrumSignatureFormat = electrumSignatureFormat;
}
private boolean isValidAddress() {
try {
getAddress();
return true;
Address address = getAddress();
return address.getScriptType() != ScriptType.P2TR;
} catch (InvalidAddressException e) {
return false;
}
@@ -211,7 +299,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(wallet.containsSeeds()) {
if(wallet.containsPrivateKeys()) {
if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
@@ -227,9 +315,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
signature.clear();
signature.appendText(signatureText);
privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
@@ -274,6 +364,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
alert.setTitle("Verification Succeeded");
alert.setHeaderText("Verification Succeeded");
alert.setContentText("The signature verified against the message.");
AppServices.moveToActiveWindowScreen(alert);
alert.showAndWait();
} else {
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
@@ -308,20 +399,21 @@ 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());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
signUnencryptedKeystore(decryptedWallet);
decryptedWallet.clearPrivate();
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
}
@@ -0,0 +1,167 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import org.controlsfx.glyphfont.Glyph;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
setContentDisplay(ContentDisplay.LEFT);
setGraphicTextGap(8);
getStyleClass().add("mixstatus-cell");
}
@Override
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
super.updateItem(mixStatus, empty);
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
if(empty || mixStatus == null) {
setText(null);
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getMixProgress() != null) {
Tooltip tooltip = new Tooltip();
tooltip.setText("Pool: " + mixStatus.getMixProgress().getPoolId().replace("btc", " BTC"));
setTooltip(tooltip);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
} else {
setGraphic(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) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage());
setTooltip(tt);
} else {
setGraphic(null);
}
}
private void setMixProgress(MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
setTooltip(tt);
} else {
setGraphic(null);
}
}
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);
}
}
}
}
@@ -8,6 +8,8 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
@@ -18,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;
@@ -34,7 +37,6 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
@@ -56,6 +58,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private SimpleListProperty<String> wordEntriesProperty;
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
private IntegerProperty defaultWordSizeProperty;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
@@ -67,7 +70,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED.getType() + ".png");
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
@@ -85,15 +88,20 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Set Words Size");
enterMnemonicButton.setText("Enter 24 Words");
defaultWordSizeProperty = new SimpleIntegerProperty(24);
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
enterMnemonicButton.setText("Enter " + newValue + " Words");
});
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(24);
enterMnemonic(defaultWordSizeProperty.get());
});
int[] numberWords = new int[] {24, 21, 18, 15, 12};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem(numberWords[i] + " words");
MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words");
final int words = numberWords[i];
item.setOnAction(event -> {
defaultWordSizeProperty.set(words);
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
@@ -110,7 +118,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
});
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"};
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import 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]);
@@ -151,15 +159,21 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
WordEntry wordEntry = new WordEntry(i, wordEntryList);
wordsPane.getChildren().add(wordEntry);
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
@@ -202,14 +216,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
calculateButton = new Button("Calculate Seed");
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Calculate the seed from the provided word list"));
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
backButton = new Button("Back");
backButton.setOnAction(event -> {
@@ -360,7 +374,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importButton.setDisable(false);
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(true);
showHideLink.setVisible(false);
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
}
}
@@ -389,6 +403,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
@@ -398,7 +414,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();
@@ -417,13 +455,18 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
nextField.requestFocus();
}
});
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
wordEntryList.set(wordNumber, newValue);
@@ -436,6 +479,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return wordField;
}
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
@@ -453,10 +504,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.equals(request.getUserText())) {
return Collections.emptyList();
}
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
@@ -468,13 +515,15 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
private class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
public PassphraseEntry() {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label passphraseLabel = new Label("Passphrase:");
CustomTextField passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
@@ -484,6 +533,10 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
public TextField getEditor() {
return passphraseField;
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
@@ -493,13 +546,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
Button importDerivationButton = new Button("Import");
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -508,7 +562,8 @@ public class MnemonicKeystoreImportPane 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();
@@ -33,9 +33,12 @@ 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;
private final UR ur;
private final UREncoder encoder;
@@ -91,8 +94,9 @@ public class QRDisplayDialog extends Dialog<UR> {
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
}
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
@@ -113,8 +117,9 @@ public class QRDisplayDialog extends Dialog<UR> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
@@ -135,7 +140,7 @@ public class QRDisplayDialog extends Dialog<UR> {
private Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, 480, 480);
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
@@ -227,7 +232,7 @@ public class QRDisplayDialog extends Dialog<UR> {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -1,9 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamEvent;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
@@ -41,6 +38,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
@@ -74,6 +72,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
@@ -82,7 +82,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
@@ -104,6 +104,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
Platform.runLater(() -> {
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
if(device.getName().equals(Config.get().getWebcamDevice())) {
webcamDeviceProperty.set(device);
}
}
}
});
});
VBox vBox = new VBox(20);
@@ -113,7 +122,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
Throwable exception = failedEvent.getSource().getException();
Throwable nested = exception;
while(nested.getCause() != null) {
nested = nested.getCause();
}
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;
Platform.runLater(() -> setResult(new Result(result)));
});
webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
@@ -122,6 +146,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
webcamService.cancel();
});
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
Config.get().setWebcamDevice(newValue.getName());
if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel();
}
});
setOnCloseRequest(event -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
@@ -134,9 +164,11 @@ 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);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
}
@@ -167,7 +199,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
} else {
decoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete()));
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
@@ -196,7 +228,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(parts.stream().filter(Objects::nonNull).count() == n) {
String complete = String.join("", parts);
try {
PSBT psbt = PSBT.fromString(complete);
PSBT psbt = PSBT.fromString(complete, false);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
@@ -215,7 +247,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//ignore, bytes not parsable as tx
}
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
result = new Result(complete);
}
} else {
PSBT psbt;
@@ -248,7 +280,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
try {
psbt = PSBT.fromString(qrtext);
psbt = PSBT.fromString(qrtext, false);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
@@ -260,7 +292,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
try {
psbt = new PSBT(qrResult.getRawBytes());
psbt = new PSBT(qrResult.getRawBytes(), false);
result = new Result(psbt);
return;
} catch(Exception e) {
@@ -285,7 +317,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//Try Base43 used by Electrum
try {
psbt = new PSBT(Base43.decode(qrtext));
psbt = new PSBT(Base43.decode(qrtext), false);
result = new Result(psbt);
return;
} catch(Exception e) {
@@ -311,7 +343,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(urRegistryType.equals(RegistryType.BYTES)) {
byte[] urBytes = (byte[])ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(urBytes);
PSBT psbt = new PSBT(urBytes, false);
return new Result(psbt);
} catch(PSBTParseException e) {
if(PSBT.isPSBT(urBytes)) {
@@ -341,7 +373,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
PSBT psbt = new PSBT(cryptoPSBT.getPsbt(), false);
return new Result(psbt);
} catch(Exception e) {
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
@@ -456,7 +488,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
wallet.setScriptType(outputDescriptor.getScriptType());
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
wallets.add(wallet);
@@ -465,7 +497,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
return wallets;
}
private ScriptType getScriptType(List<ScriptExpression> expressions) {
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) {
expressions.remove(expressions.size() - 1);
}
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
return ScriptType.P2PKH;
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
@@ -502,6 +539,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
@@ -540,10 +578,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
});
button = hd;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
devicesCombo.setConverter(new StringConverter<>() {
@Override
public String toString(WebcamDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
}
@Override
public WebcamDevice fromString(String string) {
throw new UnsupportedOperationException();
}
});
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
button = devicesCombo;
} else {
button = super.createButton(buttonType);
}
if(button instanceof Region) {
((Region)button).setPrefWidth(150);
((Region)button).setMaxWidth(150);
}
button.disableProperty().bind(webcamService.openingProperty());
return button;
}
@@ -552,7 +612,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
@@ -709,6 +769,42 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
public static class WebcamDependencyException extends ScanException {
public WebcamDependencyException() {
super();
}
public WebcamDependencyException(String message) {
super(message);
}
public WebcamDependencyException(Throwable cause) {
super(cause);
}
public WebcamDependencyException(String message, Throwable cause) {
super(message, cause);
}
}
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);
@@ -44,6 +44,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(150 + height);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
}
@@ -0,0 +1,302 @@
package com.sparrowwallet.sparrow.control;
import com.csvreader.CsvReader;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.util.StringConverter;
import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit;
private final SpreadsheetView spreadsheetView;
public static final AddressCellType ADDRESS = new AddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
this.bitcoinUnit = bitcoinUnit;
final DialogPane dialogPane = new SendToManyDialogPane();
setDialogPane(dialogPane);
setTitle("Send to Many");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
Image image = new Image("/image/sparrow-small.png");
dialogPane.setGraphic(new ImageView(image));
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
Grid grid = getGrid(initialPayments);
spreadsheetView = new SpreadsheetView(grid);
spreadsheetView.getColumns().get(0).setPrefWidth(400);
spreadsheetView.getColumns().get(1).setPrefWidth(150);
spreadsheetView.getColumns().get(2).setPrefWidth(247);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(spreadsheetView);
dialogPane.setContent(stackPane);
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(loadCsvButtonType);
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
});
dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
private Grid getGrid(List<Payment> payments) {
int rowCount = payments.size();
int columnCount = 3;
GridBase grid = new GridBase(rowCount, columnCount);
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
for(int row = 0; row < grid.getRowCount(); ++row) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
addressCell.getStyleClass().add("fixed-width");
list.add(addressCell);
double amount = (double)payments.get(row).getAmount();
if(bitcoinUnit == BitcoinUnit.BTC) {
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
}
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
amountCell.getStyleClass().add("number-value");
if(Platform.getCurrent() == Platform.OSX) {
amountCell.getStyleClass().add("number-field");
}
list.add(amountCell);
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
rows.add(list);
}
grid.setRows(rows);
grid.getColumnHeaders().setAll("Address", "Amount (" + bitcoinUnit.getLabel() + ")", "Label");
return grid;
}
private List<Payment> getPayments() {
List<Payment> payments = new ArrayList<>();
Grid grid = spreadsheetView.getGrid();
String firstLabel = null;
for(int row = 0; row < grid.getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
Address address = (Address)rowCells.get(0).getItem();
Double value = (Double)rowCells.get(1).getItem();
String label = (String)rowCells.get(2).getItem();
if(firstLabel == null) {
firstLabel = label;
}
if(label == null || label.isEmpty()) {
label = firstLabel;
}
if(address != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
payments.add(new Payment(address, label, value.longValue(), false));
}
}
return payments;
}
private class SendToManyDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button loadButton = new Button(buttonType.getText());
loadButton.setGraphicTextGap(5);
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(loadButton, buttonData);
loadButton.setOnAction(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open CSV");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("CSV", "*.csv")
);
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
if(file != null) {
try {
List<Payment> csvPayments = new ArrayList<>();
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
CsvReader csvReader = new CsvReader(reader);
while(csvReader.readRecord()) {
if(csvReader.getColumnCount() < 2) {
continue;
}
try {
long amount;
if(bitcoinUnit == BitcoinUnit.BTC) {
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
} else {
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
}
Address address = Address.fromString(csvReader.get(0));
String label = csvReader.get(2);
csvPayments.add(new Payment(address, label, amount, false));
} catch(NumberFormatException e) {
//ignore and continue - probably a header line
} catch(InvalidAddressException e) {
AppServices.showErrorDialog("Invalid Address", e.getMessage());
}
}
if(csvPayments.isEmpty()) {
AppServices.showErrorDialog("No recipients found", "No valid recipients were found. Use a CSV file with three columns, and ensure amounts are in " + bitcoinUnit.getLabel() + ".");
return;
}
spreadsheetView.setGrid(getGrid(csvPayments));
}
} catch(IOException e) {
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
}
}
});
button = loadButton;
} else {
button = super.createButton(buttonType);
}
return button;
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
public static class AddressCellType extends SpreadsheetCellType<Address> {
public AddressCellType() {
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
@Override
public String toString(Address item) {
return toStringFormat(item, ""); //$NON-NLS-1$
}
@Override
public Address fromString(String str) {
if(str == null || str.isEmpty()) { //$NON-NLS-1$
return null;
} else {
return myConverter.fromString(str);
}
}
@Override
public String toStringFormat(Address item, String format) {
try {
if(item == null) {
return ""; //$NON-NLS-1$
} else {
return item.toString();
}
} catch (Exception ex) {
return myConverter.toString(item);
}
}
});
}
public AddressCellType(StringConverter<Address> converter) {
super(converter);
}
@Override
public String toString() {
return "address";
}
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
final Address value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value);
return cell;
}
@Override
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
return new SpreadsheetCellEditor.StringEditor(view);
}
@Override
public boolean match(Object value, Object... options) {
if(value instanceof Address)
return true;
else {
try {
converter.fromString(value == null ? null : value.toString());
return true;
} catch (Exception e) {
return false;
}
}
}
@Override
public Address convertValue(Object value) {
if(value instanceof Address)
return (Address)value;
else {
try {
return converter.fromString(value == null ? null : value.toString());
} catch (Exception e) {
return null;
}
}
}
@Override
public String toString(Address item) {
return converter.toString(item);
}
@Override
public String toString(Address item, String format) {
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
}
};
}
@@ -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));
}
}
@@ -1,15 +1,25 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.scene.Node;
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 org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
public class TextAreaDialog extends Dialog<String> {
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
private final TextArea textArea;
private final String defaultValue;
@@ -17,8 +27,13 @@ public class TextAreaDialog extends Dialog<String> {
this("");
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
final DialogPane dialogPane = getDialogPane();
public TextAreaDialog(String defaultValue) {
this(defaultValue, true);
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
final DialogPane dialogPane = new TextAreaDialogPane();
setDialogPane(dialogPane);
Image image = new Image("/image/sparrow-small.png");
dialogPane.setGraphic(new ImageView(image));
@@ -28,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);
@@ -38,7 +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);
}
Platform.runLater(textArea::requestFocus);
@@ -49,6 +71,7 @@ public class TextAreaDialog extends Dialog<String> {
dialogPane.setPrefWidth(700);
dialogPane.setPrefHeight(400);
AppServices.moveToActiveWindowScreen(this);
}
public final TextArea getEditor() {
@@ -58,4 +81,54 @@ public class TextAreaDialog extends Dialog<String> {
public final String getDefaultValue() {
return defaultValue;
}
private class TextAreaDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button scanButton = new Button(buttonType.getText());
scanButton.setGraphicTextGap(5);
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(scanButton, buttonData);
scanButton.setOnAction(event -> {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.payload != null) {
textArea.setText(result.payload);
} else if(result.psbt != null) {
textArea.setText(result.psbt.toBase64String());
} else if(result.transaction != null) {
textArea.setText(Utils.bytesToHex(result.transaction.bitcoinSerialize()));
} else if(result.uri != null) {
textArea.setText(result.uri.toString());
} else if(result.extendedKey != null) {
textArea.setText(result.extendedKey.getExtendedKey());
} else if(result.outputDescriptor != null) {
textArea.setText(result.outputDescriptor.toString(true));
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
}
}
});
button = scanButton;
} else {
button = super.createButton(buttonType);
}
return button;
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
}
@@ -16,6 +16,7 @@ import java.util.Arrays;
import java.util.OptionalDouble;
public class TitledDescriptionPane extends TitledPane {
private Label mainLabel;
private Label descriptionLabel;
protected Hyperlink showHideLink;
protected HBox buttonBox;
@@ -50,7 +51,7 @@ public class TitledDescriptionPane extends TitledPane {
VBox labelsBox = new VBox();
labelsBox.setSpacing(5);
labelsBox.setAlignment(Pos.CENTER_LEFT);
Label mainLabel = new Label();
mainLabel = new Label();
mainLabel.setText(title);
mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel);
@@ -99,6 +100,10 @@ public class TitledDescriptionPane extends TitledPane {
return null;
}
public String getTitle() {
return mainLabel.getText();
}
protected void setDescription(String text) {
descriptionLabel.getStyleClass().remove("description-error");
descriptionLabel.getStyleClass().add("description-label");
@@ -0,0 +1,90 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class TorStatusLabel extends Label {
private static final Logger log = LoggerFactory.getLogger(TorStatusLabel.class);
private final TorConnectionTest torConnectionTest = new TorConnectionTest();
public TorStatusLabel() {
getStyleClass().add("tor-status");
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
setGraphic(getIcon());
update();
}
public void update() {
boolean proxyInUse = AppServices.isUsingProxy();
boolean internal = AppServices.isTorRunning();
if(!proxyInUse || internal) {
torConnectionTest.cancel();
if(internal) {
setTooltip(new Tooltip("Internal Tor proxy enabled"));
}
} else if(!torConnectionTest.isRunning()) {
torConnectionTest.setPeriod(Duration.seconds(20.0));
torConnectionTest.setBackoffStrategy(null);
torConnectionTest.setOnSucceeded(workerStateEvent -> {
getStyleClass().remove("failure");
setTooltip(new Tooltip("External Tor proxy enabled"));
});
torConnectionTest.setOnFailed(workerStateEvent -> {
if(!getStyleClass().contains("failure")) {
getStyleClass().add("failure");
}
setTooltip(new Tooltip("External Tor proxy error: " + workerStateEvent.getSource().getException().getMessage()));
log.warn("Failed to connect to external Tor proxy: " + workerStateEvent.getSource().getException().getMessage());
});
torConnectionTest.start();
}
}
private Node getIcon() {
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
adjust.setRotate(180);
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
bullseye.setFontSize(14);
Group group = new Group();
group.getChildren().addAll(adjust, bullseye);
return group;
}
private static class TorConnectionTest extends ScheduledService<Void> {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() throws IOException {
HostAndPort proxyHostAndPort = HostAndPort.fromString(Config.get().getProxyServer());
Socket socket = new Socket(proxyHostAndPort.getHost(), proxyHostAndPort.getPort());
socket.close();
return null;
}
};
}
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.uri.BitcoinURI;
@@ -10,6 +11,7 @@ import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -49,6 +51,30 @@ public class TransactionDiagram extends GridPane {
}
}
public void update(String message) {
setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight());
getChildren().clear();
VBox messagePane = new VBox();
messagePane.setPrefHeight(getDiagramHeight());
messagePane.setPadding(new Insets(0, 10, 0, 280));
messagePane.setAlignment(Pos.CENTER);
messagePane.getChildren().add(createSpacer());
Label messageLabel = new Label(message);
messagePane.getChildren().add(messageLabel);
messagePane.getChildren().add(createSpacer());
GridPane.setConstraints(messagePane, 3, 0);
getChildren().add(messagePane);
}
public void clear() {
getChildren().clear();
}
public void update() {
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = getDisplayedUtxos();
@@ -136,28 +162,28 @@ public class TransactionDiagram extends GridPane {
topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(0);
topYaxis.setEndY(10);
topYaxis.getStyleClass().add("inputs-type");
Line topBracket = new Line();
topBracket.setStartX(width * 0.5);
topBracket.setStartY(0);
topBracket.setStartY(10);
topBracket.setEndX(width);
topBracket.setEndY(0);
topBracket.setEndY(10);
topBracket.getStyleClass().add("inputs-type");
Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(getDiagramHeight());
bottomYaxis.setStartY(getDiagramHeight() - 10);
bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(getDiagramHeight());
bottomBracket.setStartY(getDiagramHeight() - 10);
bottomBracket.setEndX(width);
bottomBracket.setEndY(getDiagramHeight());
bottomBracket.setEndY(getDiagramHeight() - 10);
bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
@@ -176,6 +202,7 @@ public class TransactionDiagram extends GridPane {
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
VBox inputsBox = new VBox();
inputsBox.setMaxWidth(150);
inputsBox.setPrefWidth(150);
inputsBox.setPadding(new Insets(0, 10, 0, 10));
inputsBox.minHeightProperty().bind(minHeightProperty());
inputsBox.setAlignment(Pos.CENTER_RIGHT);
@@ -317,7 +344,7 @@ public class TransactionDiagram extends GridPane {
group.getChildren().add(yaxisLine);
double width = 140.0;
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
@@ -350,12 +377,13 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(createSpacer());
for(Payment payment : displayedPayments) {
boolean isConsolidation = walletTx.isConsolidationSend(payment);
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
Glyph outputGlyph = getOutputGlyph(payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip);
@@ -363,15 +391,35 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(createSpacer());
}
if(walletTx.getChangeNode() != null) {
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, getChangeGlyph());
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
WalletNode changeNode = changeEntry.getKey();
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox();
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString());
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip);
outputsBox.getChildren().add(changeLabel);
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
});
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.getStyleClass().add("replace-change-label");
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
outputsBox.getChildren().add(actionBox);
outputsBox.getChildren().add(createSpacer());
}
@@ -421,6 +469,24 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
public Glyph getOutputGlyph(Payment payment) {
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();
} else if(walletTx.isBadbankSend(payment)) {
return getBadbankGlyph();
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this);
}
return getPaymentGlyph();
}
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
@@ -442,6 +508,34 @@ public class TransactionDiagram extends GridPane {
return consolidationGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
premixGlyph.setFontSize(12);
return premixGlyph;
}
public static Glyph getBadbankGlyph() {
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
badbankGlyph.getStyleClass().add("badbank-icon");
badbankGlyph.setFontSize(12);
return badbankGlyph;
}
public static Glyph getWhirlpoolFeeGlyph() {
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
whirlpoolFeeGlyph.setFontSize(12);
return whirlpoolFeeGlyph;
}
public static Glyph getFakeMixGlyph() {
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
fakeMixGlyph.getStyleClass().add("fakemix-icon");
fakeMixGlyph.setFontSize(12);
return fakeMixGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
@@ -460,6 +554,20 @@ public class TransactionDiagram extends GridPane {
return changeGlyph;
}
public static Glyph getChangeWarningGlyph() {
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
changeWarningGlyph.getStyleClass().add("change-warning-icon");
changeWarningGlyph.setFontSize(12);
return changeWarningGlyph;
}
public static Glyph getChangeReplaceGlyph() {
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
changeReplaceGlyph.setFontSize(12);
return changeReplaceGlyph;
}
private Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
@@ -518,6 +626,20 @@ public class TransactionDiagram extends GridPane {
this.additionalPayments = additionalPayments;
}
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
Glyph glyph = null;
for(Payment payment : additionalPayments) {
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
return getPaymentGlyph();
}
glyph = paymentGlyph;
}
return glyph;
}
public String toString() {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
}
@@ -3,26 +3,44 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.stage.Popup;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.event.MouseOverTextEvent;
import org.fxmisc.richtext.model.StyleSpan;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.time.Duration;
import java.util.*;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
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() {
super();
addPopupHandler();
}
public void setTransaction(Transaction transaction) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
transaction.bitcoinSerializeToStream(baos);
String hex = Utils.bytesToHex(baos.toByteArray());
fullHex = Utils.bytesToHex(baos.toByteArray());
String hex = fullHex;
if(hex.length() > TRUNCATE_AT) {
hex = hex.substring(0, TRUNCATE_AT);
hex += "[truncated]";
@@ -31,6 +49,7 @@ public class TransactionHexArea extends CodeArea {
clear();
appendText(hex);
previousSegmentList = new ArrayList<>();
setContextMenu(new TransactionHexContextMenu(fullHex));
} catch (IOException e) {
throw new IllegalStateException("Can't happen");
}
@@ -44,12 +63,33 @@ public class TransactionHexArea extends CodeArea {
previousSegmentList = segments;
}
private void addPopupHandler() {
Popup popup = new Popup();
Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg);
setMouseOverTextDelay(Duration.ofMillis(150));
addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
Position position = getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex() + 1, Backward);
StyleSpan<Collection<String>> styleSpan = getParagraph(0).getStyleSpans().getStyleSpan(position.getMajor());
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeTransactionPart(styleSpan.getStyle()));
if(!popupMsg.getText().isEmpty()) {
popup.show(this, pos.getX(), pos.getY() + 10);
}
});
addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide();
});
}
private void applyHighlighting(int start, List<TransactionSegment> segments) {
int end = Math.min(segments.size(), start + SEGMENTS_INTERVAL);
for(int i = start; i < end; i++) {
TransactionSegment segment = segments.get(i);
if(segment.start < TRUNCATE_AT) {
setStyleClass(segment.start, Math.min(TRUNCATE_AT, segment.start + segment.length), segment.style);
setStyle(segment.start, Math.min(TRUNCATE_AT, segment.start + segment.length), getStyles(segment));
}
}
@@ -60,6 +100,18 @@ public class TransactionHexArea extends CodeArea {
}
}
private Collection<String> getStyles(TransactionSegment segment) {
List<String> styles = new ArrayList<>();
styles.add(segment.style);
if(segment.index != null) {
styles.add("index-" + segment.index);
}
if(segment.witnessIndex != null) {
styles.add("witnessindex-" + segment.witnessIndex);
}
return Collections.unmodifiableList(styles);
}
public List<TransactionSegment> getTransactionSegments(Transaction transaction, int selectedInputIndex, int selectedOutputIndex) {
List<TransactionSegment> segments = new ArrayList<>();
@@ -82,12 +134,12 @@ public class TransactionHexArea extends CodeArea {
//Inputs
for(int i = 0; i < transaction.getInputs().size(); i++) {
TransactionInput input = transaction.getInputs().get(i);
cursor = addSegment(segments, cursor, 32 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "hash"));
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "index"));
cursor = addSegment(segments, cursor, 32 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "hash"));
cursor = addSegment(segments, cursor, 4 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "index"));
VarInt scriptLen = new VarInt(input.getScriptBytes().length);
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript"));
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sequence"));
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript"));
cursor = addSegment(segments, cursor, 4 * 2, i, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sequence"));
}
//Number of outputs
@@ -97,10 +149,10 @@ public class TransactionHexArea extends CodeArea {
//Outputs
for(int i = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput output = transaction.getOutputs().get(i);
cursor = addSegment(segments, cursor, 8 * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "value"));
cursor = addSegment(segments, cursor, 8 * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "value"));
VarInt scriptLen = new VarInt(output.getScriptBytes().length);
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript"));
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, i, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript"));
}
if(transaction.hasWitnesses()) {
@@ -109,11 +161,12 @@ public class TransactionHexArea extends CodeArea {
if (input.hasWitness()) {
TransactionWitness witness = input.getWitness();
VarInt witnessCount = new VarInt(witness.getPushCount());
cursor = addSegment(segments, cursor, witnessCount.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "count"));
for (byte[] push : witness.getPushes()) {
cursor = addSegment(segments, cursor, witnessCount.getSizeInBytes() * 2, i, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "count"));
for(int j = 0; j < witness.getPushes().size(); j++) {
byte[] push = witness.getPushes().get(j);
VarInt witnessLen = new VarInt(push.length);
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
}
}
}
@@ -131,7 +184,15 @@ public class TransactionHexArea extends CodeArea {
}
private int addSegment(List<TransactionSegment> segments, int start, int length, String style) {
segments.add(new TransactionSegment(start, length, style));
return addSegment(segments, start, length, null, style);
}
private int addSegment(List<TransactionSegment> segments, int start, int length, Integer index, String style) {
return addSegment(segments, start, length, index, null, style);
}
private int addSegment(List<TransactionSegment> segments, int start, int length, Integer index, Integer witnessIndex, String style) {
segments.add(new TransactionSegment(start, length, index, witnessIndex, style));
return start + length;
}
@@ -143,15 +204,71 @@ public class TransactionHexArea extends CodeArea {
return "other";
}
private String describeTransactionPart(Collection<String> styles) {
String style = "";
Integer index = null;
Integer witnessIndex = null;
Iterator<String> iter = styles.iterator();
if(iter.hasNext()) {
style = iter.next();
}
while(iter.hasNext()) {
String indexStyle = iter.next();
if(indexStyle.startsWith("index-")) {
index = Integer.parseInt(indexStyle.substring("index-".length()));
}
if(indexStyle.startsWith("witnessindex-")) {
witnessIndex = Integer.parseInt(indexStyle.substring("witnessindex-".length()));
}
}
return switch(style) {
case "version" -> "Transaction version";
case "segwit-marker" -> "Segwit marker";
case "segwit-flag" -> "Segwit flag";
case "num-inputs" -> "Number of inputs";
case "input-hash" -> "Input #" + index + " outpoint txid";
case "input-index" -> "Input #" + index + " outpoint index";
case "input-sigscript-length" -> "Input #" + index + " scriptSig length";
case "input-sigscript" -> "Input #" + index + " scriptSig";
case "input-sequence" -> "Input #" + index + " sequence";
case "num-outputs" -> "Number of outputs";
case "output-value" -> "Output #" + index + " value";
case "output-pubkeyscript-length" -> "Output #" + index + " scriptPubKey length";
case "output-pubkeyscript" -> "Output #" + index + " scriptPubKey";
case "witness-count" -> "Input #" + index + " witness count";
case "witness-length" -> "Input #" + index + " witness #" + witnessIndex + " length";
case "witness-data" -> "Input #" + index + " witness #" + witnessIndex + " data";
case "locktime" -> "Locktime";
default -> "";
};
}
@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, String style) {
public TransactionSegment(int start, int length, Integer index, Integer witnessIndex, String style) {
this.start = start;
this.length = length;
this.index = index;
this.witnessIndex = witnessIndex;
this.style = style;
}
public int start;
public int length;
public Integer index;
public Integer witnessIndex;
public String style;
@Override
@@ -169,4 +286,18 @@ public class TransactionHexArea extends CodeArea {
return Objects.hash(start, length, style);
}
}
private static class TransactionHexContextMenu extends ContextMenu {
public TransactionHexContextMenu(String hex) {
MenuItem copy = new MenuItem("Copy All");
copy.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hex);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copy);
}
}
}
@@ -45,12 +45,12 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(txid, Validator.combine(
Validator.createEmptyValidator("Transaction id is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID length incorrect", newValue.length() != 64),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID must be hexadecimal", !Utils.isHex(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Open Transaction", ButtonBar.ButtonData.OK_DONE);
@@ -63,5 +63,6 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
txid.setPromptText("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16");
Platform.runLater(txid::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? Sha256Hash.wrap(txid.getText()) : null);
AppServices.moveToActiveWindowScreen(this);
}
}
@@ -1,8 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
@@ -22,7 +27,23 @@ public class UsbStatusButton extends MenuButton {
public void setDevices(List<Device> devices) {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(device.getNeedsPinSent()) {
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY)) {
deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase " + (device.isNeedsPassphraseSent() ? "Off" : "On"));
toggleItem.setOnAction(event -> {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(event1 -> {
EventManager.get().post(new RequestOpenWalletsEvent());
});
togglePassphraseService.setOnFailed(event1 -> {
AppServices.showErrorDialog("Error toggling passphrase", event1.getSource().getException().getMessage());
});
togglePassphraseService.start();
});
((Menu)deviceItem).getItems().add(toggleItem);
}
if(device.isNeedsPinSent()) {
deviceItem.setGraphic(getLockIcon());
} else {
deviceItem.setGraphic(getLockOpenIcon());
@@ -44,16 +44,18 @@ public class UtxosChart extends BarChart<String, Number> {
totalUtxos = utxoDataList.size();
if(utxoDataList.size() > MAX_BARS) {
Long otherTotal = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size()).stream().mapToLong(data -> data.getYValue().longValue()).sum();
List<Data<String, Number>> otherDataList = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size());
List<Entry> otherEntries = otherDataList.stream().map(data -> (Entry)data.getExtraValue()).collect(Collectors.toList());
Long otherTotal = otherDataList.stream().mapToLong(data -> data.getYValue().longValue()).sum();
utxoDataList = utxoDataList.subList(0, MAX_BARS - 1);
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal));
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal, otherEntries));
}
for(int i = 0; i < utxoDataList.size(); i++) {
XYChart.Data<String, Number> newData = utxoDataList.get(i);
if(i < utxoSeries.getData().size()) {
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() != null && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
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);
}
} else {
@@ -72,7 +74,7 @@ public class UtxosChart extends BarChart<String, Number> {
private String getCategoryName(Entry entry) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." : entry.getLabel();
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
}
return ((UtxoEntry)entry).getDescription();
@@ -87,7 +89,8 @@ public class UtxosChart extends BarChart<String, Number> {
for(int i = 0; i < utxoSeries.getData().size(); i++) {
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
if((data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) || (data.getExtraValue() == null && entries.size() == totalUtxos)) {
if((data.getExtraValue() instanceof Entry && entries.contains((Entry)data.getExtraValue())) ||
(data.getExtraValue() instanceof List && entries.containsAll((List)data.getExtraValue()))) {
Node bar = lookup(".data" + i);
if(bar != null) {
bar.getStyleClass().add("selected");
@@ -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) {
@@ -31,6 +31,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HISTORY);
wallet.setFontSize(50);
@@ -51,10 +52,10 @@ public class WalletBirthDateDialog extends Dialog<Date> {
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(birthDatePicker, Validator.combine(
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", newValue == null)
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
@@ -11,6 +11,7 @@ import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.Comparator;
import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> {
@@ -43,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -53,12 +54,15 @@ public class WalletExportDialog extends Dialog<Wallet> {
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
exportAccordion.getPanes().add(exportPane);
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(exportAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(480);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@@ -16,6 +16,7 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import java.util.Comparator;
import java.util.List;
public class WalletImportDialog extends Dialog<Wallet> {
@@ -47,17 +48,19 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
@@ -73,6 +76,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(500);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@@ -91,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);
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1);
importAccordion.getPanes().add(0, devicePane);
}
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
@@ -44,6 +44,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(460);
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
AppServices.moveToActiveWindowScreen(this);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
wallet.setFontSize(50);
@@ -52,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);
@@ -92,6 +97,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(name, Validator.combine(
Validator.createEmptyValidator("Wallet name is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue))
@@ -99,7 +105,6 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
validationSupport.registerValidator(existingPicker, Validator.combine(
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", existingCheck.isSelected() && newValue == null)
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE);
@@ -45,6 +45,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(260);
AppServices.moveToActiveWindowScreen(this);
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lock.setFontSize(50);
@@ -76,8 +77,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.registerValidator(passwordConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Password confirmation does not match", !passwordConfirm.getText().equals(password.getText())));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(passwordConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Password confirmation does not match", !passwordConfirm.getText().equals(password.getText())));
});
okButtonType = new javafx.scene.control.ButtonType(requirement.okButtonText, ButtonBar.ButtonData.OK_DONE);
@@ -14,6 +14,7 @@ import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -312,6 +313,23 @@ public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess
return this.fps;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
WebcamScanDevice that = (WebcamScanDevice) o;
return Objects.equals(fullname, that.fullname);
}
@Override
public int hashCode() {
return Objects.hash(fullname);
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
@@ -3,20 +3,43 @@ package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
private static boolean rescan;
@Override
public List<WebcamDevice> getDevices() {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
if(rescan || webcamDevices.isEmpty()) {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
scanDevices.add(scanDevice);
}
}
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
newDevices.removeAll(webcamDevices);
webcamDevices.addAll(newDevices);
webcamDevices.removeIf(device -> !scanDevices.contains(device));
}
return scanDevices;
return webcamDevices;
}
public static ObservableList<WebcamDevice> getFoundDevices() {
return webcamDevices;
}
public static void rescan() {
rescan = true;
}
}
@@ -1,12 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.github.sarxos.webcam.WebcamUpdater;
import com.github.sarxos.webcam.*;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -23,26 +22,30 @@ import java.util.concurrent.TimeUnit;
public class WebcamService extends ScheduledService<Image> {
private WebcamResolution resolution;
private WebcamDevice device;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
private Webcam cam;
private long lastQrSampleTime;
private final Reader qrReader;
static {
Webcam.setDriver(new WebcamScanDriver());
}
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution;
this.device = device;
this.listener = listener;
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader();
}
@Override
@@ -58,6 +61,23 @@ public class WebcamService extends ScheduledService<Image> {
}
cam = webcams.get(0);
if(device != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(device.getName())) {
cam = webcam;
}
}
} else if(Config.get().getWebcamDevice() != null) {
for(Webcam webcam : webcams) {
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
cam = webcam;
}
}
}
device = cam.getDevice();
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
@@ -70,6 +90,10 @@ public class WebcamService extends ScheduledService<Image> {
}
BufferedImage bimg = cam.getImage();
if(bimg == null) {
return null;
}
Image image = SwingFXUtils.toFXImage(bimg, null);
updateValue(image);
@@ -106,9 +130,9 @@ public class WebcamService extends ScheduledService<Image> {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
Result result = qrReader.decode(bitmap);
resultProperty.set(result);
} catch(NotFoundException e) {
} catch(ReaderException e) {
// fall thru, it means there is no QR code in image
}
}
@@ -133,6 +157,14 @@ public class WebcamService extends ScheduledService<Image> {
this.resolution = resolution;
}
public WebcamDevice getDevice() {
return device;
}
public void setDevice(WebcamDevice device) {
this.device = device;
}
public boolean isOpening() {
return opening.get();
}
@@ -0,0 +1,209 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreXprvImport;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.List;
public class XprvKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
protected final KeystoreXprvImport importer;
private Button enterXprvButton;
private SplitMenuButton importButton;
private ExtendedKey xprv;
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer) {
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
createImportButton();
buttonBox.getChildren().add(importButton);
}
public XprvKeystoreImportPane(Keystore keystore) {
super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
try {
this.xprv = keystore.getExtendedMasterPrivateKey();
} catch(MnemonicException e) {
//can't happen
}
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
setContent(getXprvEntry(true));
setExpanded(true);
}
@Override
protected Control createButton() {
enterXprvButton = new Button("Enter Private Key");
enterXprvButton.managedProperty().bind(enterXprvButton.visibleProperty());
enterXprvButton.setOnAction(event -> {
enterXprvButton.setDisable(true);
enterXprv();
});
return enterXprvButton;
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.getStyleClass().add("default-button");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation());
});
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import 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);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private void enterXprv() {
setDescription("Enter master private key");
showHideLink.setVisible(false);
setContent(getXprvEntry(false));
setExpanded(true);
}
private void importKeystore(List<ChildNumber> derivation) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(derivation, xprv);
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch (ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
}
private Node getXprvEntry(boolean displayOnly) {
TextArea xprvField = new TextArea();
xprvField.setPrefRowCount(2);
xprvField.setWrapText(true);
xprvField.getStyleClass().add("fixed-width");
xprvField.setPromptText(ExtendedKey.Header.fromScriptType(ScriptType.P2PKH, true).getName() + (wallet != null ? "/" + ExtendedKey.Header.fromScriptType(wallet.getScriptType(), true).getName() : "") + "...");
HBox.setHgrow(xprvField, Priority.ALWAYS);
if(xprv != null) {
xprvField.setText(xprv.toString());
}
if(displayOnly) {
xprvField.setEditable(false);
}
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(xprvField, Validator.combine(
Validator.createEmptyValidator("xprv is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid private key", !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly())
));
Button importXprvButton = new Button("Import");
importXprvButton.setMinWidth(80);
importXprvButton.setDisable(true);
importXprvButton.setOnAction(event -> {
enterXprvButton.setVisible(false);
importButton.setVisible(true);
setDescription("Ready to import");
xprv = ExtendedKey.fromDescriptor(xprvField.getText());
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
});
xprvField.textProperty().addListener((observable, oldValue, newValue) -> {
importXprvButton.setDisable(newValue.isEmpty() || !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly());
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(xprvField);
if(!displayOnly) {
contentBox.getChildren().add(importXprvButton);
}
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(100);
return contentBox;
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKeystore(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, 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();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -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,8 @@
package com.sparrowwallet.sparrow.event;
/**
* Empty class to indicate the includeMempoolOutputs config setting has changed
*/
public class IncludeMempoolOutputsChangedEvent {
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List;
public class KeystoreEncryptionChangedEvent extends WalletSettingsChangedEvent {
private List<Keystore> changedKeystores;
public KeystoreEncryptionChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) {
super(wallet, pastWallet, walletId);
this.changedKeystores = changedKeystores;
}
public List<Keystore> getChangedKeystores() {
return changedKeystores;
}
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List;
/**
* This event is trigger when one or more keystores on a wallet are updated, and the wallet is saved
*/
public class KeystoreLabelsChangedEvent extends WalletSettingsChangedEvent {
private final List<Keystore> changedKeystores;
public KeystoreLabelsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) {
super(wallet, pastWallet, walletId);
this.changedKeystores = changedKeystores;
}
public List<Keystore> getChangedKeystores() {
return changedKeystores;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class LoadingLogChangedEvent {
private final boolean visible;
public LoadingLogChangedEvent(boolean visible) {
this.visible = visible;
}
public boolean isVisible() {
return visible;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class MixToConfigChangedEvent {
private final Wallet wallet;
public MixToConfigChangedEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -6,21 +6,25 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class NewWalletTransactionsEvent {
private final Wallet wallet;
private final List<BlockTransaction> blockTransactions;
private final List<TransactionEntry> transactionEntries;
private final long totalBlockchainValue;
private final long totalMempoolValue;
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
public NewWalletTransactionsEvent(Wallet wallet, List<TransactionEntry> transactionEntries) {
this.wallet = wallet;
this.blockTransactions = blockTransactions;
this.totalBlockchainValue = totalBlockchainValue;
this.totalMempoolValue = totalMempoolValue;
this.transactionEntries = transactionEntries;
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
}
public Wallet getWallet() {
@@ -28,7 +32,7 @@ public class NewWalletTransactionsEvent {
}
public List<BlockTransaction> getBlockTransactions() {
return blockTransactions;
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
public long getTotalValue() {
@@ -55,4 +59,13 @@ public class NewWalletTransactionsEvent {
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
public List<BlockTransaction> getWhirlpoolMixTransactions() {
List<BlockTransaction> mixTransactions = new ArrayList<>();
if(wallet.isWhirlpoolMixWallet()) {
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
}
return mixTransactions;
}
}
@@ -1,15 +1,20 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveActionEvent {
private final NodeEntry receiveEntry;
private final Wallet wallet;
public ReceiveActionEvent(NodeEntry receiveEntry) {
this.receiveEntry = receiveEntry;
this.wallet = receiveEntry.getWallet();
}
public NodeEntry getReceiveEntry() {
return receiveEntry;
public ReceiveActionEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,22 @@
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,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
public class ReplaceChangeAddressEvent {
private final WalletTransaction walletTransaction;
public ReplaceChangeAddressEvent(WalletTransaction walletTx) {
this.walletTransaction = walletTx;
}
public WalletTransaction getWalletTransaction() {
return walletTransaction;
}
}
@@ -2,17 +2,30 @@ package com.sparrowwallet.sparrow.event;
import javafx.stage.Window;
import java.io.File;
/**
* Event class used to request the transaction open file dialog
*/
public class RequestTransactionOpenEvent {
private final Window window;
private final File file;
public RequestTransactionOpenEvent(Window window) {
this.window = window;
this.file = null;
}
public RequestTransactionOpenEvent(Window window, File file) {
this.window = window;
this.file = file;
}
public Window getWindow() {
return window;
}
public File getFile() {
return file;
}
}
@@ -2,17 +2,30 @@ package com.sparrowwallet.sparrow.event;
import javafx.stage.Window;
import java.io.File;
/**
* Event class used to request the wallet open dialog
*/
public class RequestWalletOpenEvent {
private final Window window;
private final File file;
public RequestWalletOpenEvent(Window window) {
this.window = window;
this.file = null;
}
public RequestWalletOpenEvent(Window window, File file) {
this.window = window;
this.file = file;
}
public Window getWindow() {
return window;
}
public File getFile() {
return file;
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.List;
public class SendPaymentsEvent {
private final Wallet wallet;
private final List<Payment> payments;
public SendPaymentsEvent(Wallet wallet, List<Payment> payments) {
this.wallet = wallet;
this.payments = payments;
}
public Wallet getWallet() {
return wallet;
}
public List<Payment> getPayments() {
return payments;
}
}
@@ -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;
@@ -11,18 +12,29 @@ public class SpendUtxoEvent {
private final List<BlockTransactionHashIndex> utxos;
private final List<Payment> payments;
private final Long fee;
private final boolean includeMempoolInputs;
private final boolean includeSpentMempoolOutputs;
private final Pool pool;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, false);
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeMempoolInputs) {
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeSpentMempoolOutputs) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.fee = fee;
this.includeMempoolInputs = includeMempoolInputs;
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
this.pool = null;
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.fee = fee;
this.includeSpentMempoolOutputs = false;
this.pool = pool;
}
public Wallet getWallet() {
@@ -41,7 +53,11 @@ public class SpendUtxoEvent {
return fee;
}
public boolean isIncludeMempoolInputs() {
return includeMempoolInputs;
public boolean isIncludeSpentMempoolOutputs() {
return includeSpentMempoolOutputs;
}
public Pool getPool() {
return pool;
}
}
@@ -1,16 +1,20 @@
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 boolean firstRunDone = false;
private static final Map<File, Long> eventTime = new HashMap<>();
private static final Logger log = LoggerFactory.getLogger(StorageEvent.class);
public StorageEvent(File file, Action action, String status) {
private static boolean firstRunDone = false;
private static final Map<String, Long> eventTime = new HashMap<>();
public StorageEvent(String walletId, Action action, String status) {
super(action, status);
Integer keyDerivationPeriod = Config.get().getKeyDerivationPeriod();
@@ -19,11 +23,13 @@ public class StorageEvent extends TimedEvent {
}
if(action == Action.START) {
eventTime.put(file, System.currentTimeMillis());
eventTime.put(walletId, System.currentTimeMillis());
timeMills = keyDerivationPeriod;
} else if(action == Action.END) {
long start = eventTime.get(file);
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,13 @@
package com.sparrowwallet.sparrow.event;
public class UtxosChartChangedEvent {
private final boolean visible;
public UtxosChartChangedEvent(boolean visible) {
this.visible = visible;
}
public boolean isVisible() {
return visible;
}
}
@@ -11,8 +11,8 @@ import java.io.File;
* This is because any failure in saving the wallet must be immediately reported to the user.
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
*/
public class WalletAddressesChangedEvent extends WalletSettingsChangedEvent {
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
super(wallet, pastWallet, walletFile);
public class WalletAddressesChangedEvent extends WalletHistoryClearedEvent {
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
super(wallet, pastWallet, walletId);
}
}
@@ -1,21 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
/**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
* Extends WalletDataChangedEvent so triggers a background save.
*/
public class WalletEntryLabelChangedEvent extends WalletDataChangedEvent {
private final Entry entry;
public WalletEntryLabelChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
}
@@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
import java.util.List;
/**
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
*/
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
private final List<Entry> entries;
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
super(wallet);
this.entries = List.of(entry);
}
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
super(wallet);
this.entries = entries;
}
public List<Entry> getEntries() {
return entries;
}
}
@@ -23,8 +23,8 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
this.historyChangedNodes = historyChangedNodes;
}
public File getWalletFile() {
return storage.getWalletFile();
public String getWalletId() {
return storage.getWalletId(getWallet());
}
public List<WalletNode> getHistoryChangedNodes() {
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletHistoryClearedEvent extends WalletSettingsChangedEvent {
public WalletHistoryClearedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
super(wallet, pastWallet, walletId);
}
}
@@ -6,7 +6,7 @@ public class WalletHistoryFailedEvent extends WalletHistoryStatusEvent {
private final Throwable exception;
public WalletHistoryFailedEvent(Wallet wallet, Throwable exception) {
super(wallet, exception.getCause() == null ? exception.getMessage() : exception.getCause().getMessage());
super(wallet, exception.getMessage());
this.exception = exception;
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMasterMixConfigChangedEvent extends WalletMixConfigChangedEvent {
public WalletMasterMixConfigChangedEvent(Wallet wallet) {
super(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMixConfigChangedEvent extends WalletChangedEvent {
public WalletMixConfigChangedEvent(Wallet wallet) {
super(wallet);
}
}

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