Compare commits

..
376 Commits
Author SHA1 Message Date
Craig Raw 685fef6b76 shorten labels for linux 2021-04-08 09:20:35 +02:00
Craig Raw 9a4eb22396 v1.3.2 2021-04-08 08:38:16 +02:00
Craig Raw 57eb3a77e7 reduce error logging when fee and exchanges rate sources are unavailable 2021-04-08 08:34:31 +02:00
Craig Raw dcc63046bd switch to secure openimaj repo 2021-04-07 15:56:10 +02:00
Craig Raw 8388a7fed5 reduce camera cpu usage through sleeping webcam capture threads to match 10fps 2021-04-07 13:34:06 +02:00
Craig Raw d5830399b7 upgrade to hwi 2.0.1 2021-04-07 11:50:52 +02:00
Craig Raw 38b8fa3b86 add view menu item to switch hd capture 2021-04-07 10:36:18 +02:00
Craig Raw 6f26c7e138 update passport import descriptions 2021-04-07 10:01:44 +02:00
Craig Raw 9e869ae69b handle no camera detected better 2021-04-07 09:56:51 +02:00
Craig Raw 4a7b0b20ad tray changes for windows 2021-04-07 09:45:52 +02:00
Craig Raw 8fc971c07c add minimize to system tray functionality 2021-04-07 09:32:01 +02:00
Craig Raw cb884d97cb request strongest instance of securerandom when generating a seed 2021-04-06 11:46:51 +02:00
Craig Raw 942db9df48 edit passport wallet export description 2021-04-06 09:34:05 +02:00
Craig Raw 193f88b88f implement changes to export addresses functionality 2021-04-06 09:27:51 +02:00
craigrawandGitHub 6785bccf0e Merge pull request #89 from jimbojw/export-addresses
Export Addresses to CSV File
2021-04-06 08:40:43 +02:00
Craig Raw 579b9a685b automatically switch between internal and external tor proxy as required when connecting to server 2021-04-05 14:56:23 +02:00
Jimbo 57b3fd99bc Correcting address column in exported CSV, removing Derivation column. 2021-04-05 07:00:04 -04:00
Craig Raw c3ae98f3d1 add passport multisig wallet export 2021-04-05 12:19:18 +02:00
Craig Raw d635815607 reduce java-based cpu usage when scanning 2021-04-05 11:29:13 +02:00
Craig Raw e524396aaf add trezor one passphrase toggle capability 2021-04-02 15:26:43 +02:00
Craig Raw 08edc04c6d save psbts in binary format by default, add file submenu item for base64 2021-04-02 14:12:48 +02:00
Jimbo e88d6265b4 Adding export button for saving addresses as CSV 2021-04-02 06:29:15 -04:00
Craig Raw 5d91f033c0 add ssl server certificate pinning 2021-04-02 12:27:04 +02:00
Craig Raw 771bd1545c fix export of sparrow wallet from settings screen 2021-04-01 12:41:27 +02:00
Craig Raw f65fec66bb upgrade to hummingbird 1.5.5 2021-04-01 12:09:03 +02:00
Craig Raw 9c6dbeec69 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-04-01 09:52:37 +02:00
Craig Raw 563af71ed2 log errors on failed qr psbt parsing 2021-04-01 09:52:19 +02:00
craigrawandGitHub a4579c953a Merge pull request #82 from finab1/patch-1
Update README.md
2021-03-27 07:38:38 +02:00
finab1andGitHub 6a3c3de67c Update README.md
Grammer correction in README.
2021-03-27 15:40:47 +11:00
Craig Raw f8fa929166 upgrade to hummingbird 1.5.4 which handles legacy encoding in uppercase 2021-03-24 14:37:06 +02:00
Craig Raw 6aaf532051 v1.3.1 2021-03-24 13:33:41 +02:00
Craig Raw fb59bdfff4 update fee rate priority as fee prediction changes 2021-03-24 13:26:23 +02:00
Craig Raw c8aa1b0a19 increase tab icon size by 1pt 2021-03-24 13:17:49 +02:00
Craig Raw 11a201b3f5 fix webcam opening/closing issues, display progress bar for qr animation scanning progress 2021-03-24 12:59:33 +02:00
Craig Raw b74741bccb add sparrow wallet file export, dont unnecessarily ask for password when exporting 2021-03-22 14:39:58 +02:00
Craig Raw b8739ace4f remove order resources, wait for jdk16 for deterministic jlink builds across servers 2021-03-22 12:02:58 +02:00
Craig Raw cd674ee9be order resources in merged module 2021-03-22 11:31:30 +02:00
Craig Raw 8b3ed8171b fix archive issues 2021-03-20 14:48:24 +02:00
Craig Raw 28ee16279c deterministic builds on linux and windows 2021-03-20 14:13:10 +02:00
Craig Raw ff43db0842 retain messages from rpc errors when loading wallets with bwt 2021-03-20 11:49:17 +02:00
Craig Raw 8ddac9acba reduce target blocks range, predicting too far ahead leads to incorrect estimates 2021-03-19 18:34:53 +02:00
Craig Raw 0c2554da72 add export button to wallet settings 2021-03-19 14:34:29 +02:00
Craig Raw 1274d56fce ensure fees decrease as target blocks increase, show priority on target blocks slider track 2021-03-19 13:35:59 +02:00
Craig Raw 8d9e253f93 update wallet in walletform on label and keystore source changes, detect threshold change 2021-03-19 12:27:08 +02:00
Craig Raw 9166160008 always show wallet name on password dialog 2021-03-18 11:59:59 +02:00
Craig Raw 35eb8fcdf9 add suffix to rbf transaction labels to indicate transaction has been replaced 2021-03-18 11:34:50 +02:00
Craig Raw d34c8b8bde add menu command to install udev rules via hwi on linux 2021-03-17 13:53:38 +02:00
Craig Raw 80a079cbee upgrade to hwi 2.0.0-final 2021-03-17 12:58:22 +02:00
Craig Raw 5d181d1abb upgrade to bwt 0.2.3 2021-03-17 12:39:55 +02:00
Craig Raw 6fbd332b4d add preference option to load recent wallets 2021-03-17 11:59:58 +02:00
Craig Raw 211e5952aa various minor ui improvements 2021-03-17 11:39:46 +02:00
Craig Raw 4078c61d6b add tab label icons, show loading and failure status there 2021-03-16 12:07:55 +02:00
Craig Raw 8b7d1e6888 try proxy setting if internal tor cant start 2021-03-15 14:07:48 +02:00
Craig Raw 7c6daf2e11 scan and import connected devices from import wallet dialog 2021-03-15 13:03:09 +02:00
Craig Raw e1f405d886 default to hd capture on new osx installs 2021-03-15 11:25:30 +02:00
Craig Raw 1d73956853 fixes to minimum and pay then replace fee rates 2021-03-15 10:34:18 +02:00
Craig Raw 205f78151a revert to original hwi file permissions 2021-03-15 09:53:20 +02:00
Craig Raw 5853eb01f5 unpack hwi to all executable file permissions 2021-03-14 12:59:01 +02:00
Craig Raw 1a5e71f852 remove debug on logging config 2021-03-12 18:42:59 +02:00
Craig Raw 7053e7b225 fix logback conflict 2021-03-12 18:32:16 +02:00
Craig Raw 5bee26a8cc followup to jul logging bridge 2021-03-12 18:29:04 +02:00
Craig Raw b9caff352b actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:14:07 +02:00
Craig Raw 73089ce99c actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:12:42 +02:00
Craig Raw 34080ae8b3 try to quieten logs with spurious css warnings 2021-03-12 17:29:13 +02:00
Craig Raw 307c7d53f8 add error logging when enumerate fails 2021-03-12 17:19:54 +02:00
Craig Raw a6d2a6563b Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-03-12 17:15:18 +02:00
Craig Raw 577e570c99 update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 17:14:23 +02:00
Craig Raw 06411d4e2f update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 11:36:11 +02:00
Craig Raw 3e287bfb2d dont start version check service if not on mainnet 2021-03-12 10:46:59 +02:00
Craig Raw 9872fc9221 v1.3.0 2021-03-11 14:48:41 +02:00
Craig Raw 84d2db427c dont allow scheduled enumerate to run while interactive commands are running 2021-03-11 14:37:19 +02:00
Craig Raw cfbf9187e6 update linux and windows hwi binaries to post rc.2 HEAD 2021-03-11 14:01:00 +02:00
Craig Raw 5478ac05b8 show error when importing tpubs from usb on mainnet etc 2021-03-11 11:39:19 +02:00
Craig Raw 836bd6bc40 display error on sendpin correctly 2021-03-11 11:13:30 +02:00
Craig Raw 1bab07fc5f dont allow duplicate loading wallet references 2021-03-11 10:37:30 +02:00
Craig Raw 4b3de044c3 remove unused --addr_type parameter when using hwi displayaddress with --desc 2021-03-11 10:36:44 +02:00
Craig Raw f2c2fc6071 fix exception on debug logging when sending from two or more utxos on the same derivation 2021-03-11 10:33:51 +02:00
Craig Raw 4dd52eb9dd handle csv export with mempool txes 2021-03-11 10:08:41 +02:00
Craig Raw e45fdc72a0 reduce log level on electrs notification errors to quieten logs 2021-03-10 14:17:40 +02:00
Craig Raw d67dd515c4 stop progress bar when loading wallet is closed 2021-03-10 13:49:21 +02:00
Craig Raw ff962aa5f0 add overpaid indicator to fee priority 2021-03-10 13:16:08 +02:00
Craig Raw c0e44b2c55 show online docs and log file from help menu 2021-03-10 12:51:17 +02:00
Craig Raw f0c239d625 upgrade to hwi-2.0.0-rc.2, display addresses on usb devices for multisig wallets 2021-03-10 11:31:19 +02:00
Craig Raw 58a31f435e show tooltips on fee priority label, minor ui improvements 2021-03-10 11:30:06 +02:00
Craig Raw c49439d541 add menu item to show introduction again 2021-03-08 17:00:32 +02:00
Craig Raw 0070cbfb1e use and display fractional tx virtual size, indicate sizes on unfinalized txes may change 2021-03-08 16:22:22 +02:00
Craig Raw 7ed75fc83d add fee rate priority indicator 2021-03-08 15:25:03 +02:00
Craig Raw 13d701b0a7 indicate when initially loading a wallets transactions in status bar 2021-03-08 10:45:29 +02:00
Craig Raw 07ed9c857b improve placeholder text for private electrum url hostname 2021-03-08 09:36:21 +02:00
Craig Raw 32ab53ba03 append tor debug output to test results 2021-03-08 09:35:21 +02:00
Craig Raw 68344e981c set fiat value on scanning payment uri 2021-03-08 09:21:04 +02:00
Craig Raw 10c1574a4a provide framework for application logging 2021-03-08 09:18:48 +02:00
Craig Raw 3a853e2ab1 fix text width issue with overlay on linux 2021-03-05 13:18:24 +02:00
Craig Raw 0f639dc2c2 clear transport on a failed test connection, try ssl or vice-versa on electrum connection failure 2021-03-05 09:16:00 +02:00
Craig Raw a801773545 max button retains state to adjust amount as fee changes 2021-03-04 13:00:39 +02:00
Craig Raw ffbf4bb0db default to mempool size fee rates selection if not configured 2021-03-03 15:36:33 +02:00
Craig Raw 11e46155a1 indicate welcome dialog connections are demonstration only 2021-03-03 15:19:56 +02:00
Craig Raw f369b3ad00 add command line option to set log level 2021-03-01 16:32:11 +02:00
Craig Raw 6068666415 prompt to retry on invalid password 2021-03-01 15:48:47 +02:00
Craig Raw 6a58e8a799 dont lose labels when rescanning, even if app is restarted 2021-03-01 15:27:54 +02:00
Craig Raw 3bd2f69157 add change password button, delete backups option on changing password 2021-02-26 16:03:18 +02:00
Craig Raw 03ec0c9da1 v1.2.0 2021-02-25 12:08:19 +02:00
Craig Raw 47fc0e11eb test various possible derivation paths when electrum seed is provided 2021-02-25 11:54:47 +02:00
Craig Raw 27f4b3b3ae add signet support 2021-02-24 13:39:35 +02:00
Craig Raw bb8c9a19cf fix configure later path 2021-02-24 11:11:31 +02:00
Craig Raw 16da6ae224 cross-platform followup 2021-02-23 16:19:04 +02:00
Craig Raw 152b55f7f0 replace welcome dialog 2021-02-23 13:56:28 +02:00
Craig Raw 62d83151d7 add config option to disable script type derivation validation 2021-02-22 16:05:18 +02:00
Craig Raw 9060a4b284 ensure psbt files always have correct extension 2021-02-22 14:55:31 +02:00
Craig Raw 759de8e047 add public server retry period with backoff 2021-02-18 09:27:01 +02:00
Craig Raw 1e3dd61b57 followup 2021-02-17 13:57:11 +02:00
Craig Raw d13bc8c313 add additional public servers, restart immediately on another when public server connection fails 2021-02-17 13:42:19 +02:00
Craig Raw 9b6ef1f3ca change export csv icon 2021-02-17 11:18:29 +02:00
Craig Raw 173078dd8d followup to script type descriptions 2021-02-17 11:10:08 +02:00
Craig Raw ae2db6e204 improve script type descriptions and help text 2021-02-17 10:24:21 +02:00
Craig Raw 5abea8c594 followup fixes for linux 2021-02-17 10:00:26 +02:00
Craig Raw cb94d91e98 update welcome and about dialogs for public electrum server 2021-02-16 16:57:10 +02:00
Craig Raw 454e4b4c4b add public electrum server type 2021-02-16 16:38:36 +02:00
Craig Raw 48a63c25d2 adjust csv icon position 2021-02-16 12:05:57 +02:00
Craig Raw 0fdc90523e export transactions to csv 2021-02-16 11:57:34 +02:00
Craig Raw 3203308d82 maybe fix help tooltips not appearing 2021-02-16 10:08:44 +02:00
Craig Raw 83a7f5c562 v1.1.0 2021-02-11 11:41:37 +02:00
Craig Raw 55e0648fc8 fix help text to match updated cobo firmware 2021-02-11 11:16:51 +02:00
Craig Raw b517b575da add custom dmg installer background 2021-02-11 11:15:38 +02:00
Craig Raw 890b4098d7 add refresh wallet menu item 2021-02-10 10:23:42 +02:00
Craig Raw 11a0e3765b avoid losing tx, address and txo labels when performing a full wallet refresh 2021-02-09 17:32:36 +02:00
Craig Raw 3ffb09c5e6 fix java version 2021-02-08 19:58:03 +02:00
Craig Raw 622b681109 add blue wallet vault import and export and add wallet export via qr 2021-02-08 16:29:59 +02:00
Craig Raw c27a576b3d make passphrase entry a masked password field 2021-02-08 12:42:46 +02:00
Craig Raw 7bc8bdc13e provide help tooltips on policy types and script types 2021-02-08 12:31:34 +02:00
Craig Raw 0097a1ecbc also show legacy qr display for imported keystores 2021-02-08 09:36:24 +02:00
Craig Raw 525d57ec73 handle rbf fee increase for consolidation outputs 2021-02-04 12:14:42 +02:00
Craig Raw e3d7bb57ee move internal tor to a top level app service, support non-ssl proxying 2021-02-04 12:00:49 +02:00
Craig Raw 095518e858 fix bitcoin core cookie dir not present npe 2021-01-28 12:45:12 +02:00
Craig Raw ac470f64c1 revert dynamic tx detail pane 2021-01-28 12:34:32 +02:00
Craig Raw 30215b0dac dynamically resize components on window height change 2021-01-28 12:29:20 +02:00
Craig Raw 563252c823 v1.0.0 2021-01-28 11:51:38 +02:00
Craig Raw 3a7ffe95e8 fix core wallet field - disabled when connected 2021-01-28 11:41:28 +02:00
Craig Raw b588a4ee02 always show scan time remaining 2021-01-28 11:19:57 +02:00
Craig Raw ab0611fabc update bwt to latest HEAD 2021-01-28 11:02:19 +02:00
Craig Raw a121bb5f26 add passport hww single and multisig importing 2021-01-28 10:43:54 +02:00
Craig Raw 2fc01e0345 resize on initial scene change 2021-01-27 10:08:47 +02:00
Craig Raw 650c3657a5 set tx hex divider lower when window height is reduced 2021-01-27 09:16:58 +02:00
Craig Raw d836eb515e further reduce min height to 730px on all platforms 2021-01-27 08:57:46 +02:00
Craig Raw 717bc9fec2 set default core wallet name if offline mode is initially selected 2021-01-26 18:28:07 +02:00
Craig Raw 1ad6da86b1 reduce tx diagram height to fit min resolution of 768px 2021-01-26 18:25:34 +02:00
Craig Raw 54680a5692 fix bug with trezor/keepkey keystore import, show hwi errors on all related dialogs 2021-01-26 14:39:49 +02:00
Craig Raw 6ef5e79b39 greater min window height on windows and linux 2021-01-26 13:23:46 +02:00
Craig Raw 1dbf51b406 reduce min window height by 50px 2021-01-26 12:19:25 +02:00
Craig Raw 8a3aa0d06e revert to java 14.0.2 on windows due to https://bugs.openjdk.java.net/browse/JDK-8254920 2021-01-25 16:55:21 +02:00
Craig Raw ca99efa215 only open mac-specific module on mac build 2021-01-25 15:54:12 +02:00
Craig Raw cc202ce4ca minor ui tweaks 2021-01-25 15:15:10 +02:00
Craig Raw ad44fe2335 use java major version only 2021-01-25 14:44:38 +02:00
Craig Raw 452242f208 upgrade github actions jdk to 15.0.1.hs.adpt 2021-01-25 14:41:33 +02:00
Craig Raw ff83af2e0d add hd capture toggle for qr scanning 2021-01-25 14:00:17 +02:00
Craig Raw aacecc8517 initialize bwt only on connection, default to core multi-wallet use 2021-01-20 13:03:47 +02:00
Craig Raw a9a3eef157 truncate long electrum wallet labels 2021-01-18 13:39:12 +02:00
Craig Raw 7fc37c21fd v0.9.10 2021-01-14 12:35:45 +02:00
Craig Raw ff828b483b improve status bar feedback when starting bwt, syncing and scanning 2021-01-13 20:25:55 +02:00
Craig Raw 64a9cce837 log level revert and minor startup fixes 2021-01-13 16:23:02 +02:00
Craig Raw caa7ef429c add backup confirmation overlay to bip39 list generation process 2021-01-13 15:43:39 +02:00
Craig Raw 38818d8222 always set charset to utf-8 when converting from bytes to string 2021-01-13 12:17:01 +02:00
Craig Raw fd7d2232a1 only start bwt with valid wallets 2021-01-13 11:40:29 +02:00
Craig Raw b5eb59344c avoid npe on new wallet 2021-01-13 11:33:20 +02:00
Craig Raw 3093e4906c fix npe on initial startup #2 2021-01-13 11:27:55 +02:00
Craig Raw 2faddc1133 fix npe on initial startup 2021-01-13 11:23:38 +02:00
Craig Raw a2ead56593 Merge branch 'bwt' 2021-01-13 09:12:01 +02:00
Craig Raw 03adc1f9f0 fix drongo head 2021-01-13 09:10:32 +02:00
Craig Raw 7bc707bcaa update linux and windows bwt 2021-01-13 08:46:57 +02:00
Craig Raw 7ae74e3000 handle mode and startup config better 2021-01-13 08:11:10 +02:00
Craig Raw 94da3b37b6 show tx count, set default server type 2021-01-12 14:43:30 +02:00
Craig Raw c6689228d0 handle new coldcard firmware p2sh_p2wsh import name change 2021-01-12 13:50:32 +02:00
Craig Raw d847da9d61 bwt preferences and connection handling improvements 2021-01-12 13:24:11 +02:00
Craig Raw 6637ea09bf fix isconnecting with ready status flag 2021-01-12 10:37:22 +02:00
Craig Raw 99959470e2 show warning before changing a wallet with transactions 2021-01-12 10:15:42 +02:00
Craig Raw 826162ba9f various bwt related tweaks 2021-01-11 18:26:45 +02:00
Craig Raw 546ccd66b3 upgrade to bwt with earlier termination, various bitcoin core related ui changes 2021-01-11 17:31:28 +02:00
Craig Raw 628b15a3b5 support broadcasting wallet and non-wallet loaded txes 2021-01-11 12:27:28 +02:00
Craig Raw 72281233e1 Merge branch 'master' into bwt 2021-01-08 17:01:32 +02:00
Craig Raw c68722e420 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-01-08 16:11:59 +02:00
Craig Raw 901a4520fd safe multithreading config writes 2021-01-08 16:11:36 +02:00
Craig Raw 3e083b907d bwt fixes and startup status 2021-01-08 16:10:00 +02:00
Craig Raw 3c4b25ecee bwt threading and wallet rescanning 2021-01-07 13:04:42 +02:00
craigrawandGitHub c23bbb9e02 Merge pull request #55 from bitcoinhodler/grammar
Fix grammar errors in README
2021-01-01 16:24:13 +02:00
Craig Raw 94897e9744 remove folder 2021-01-01 16:15:52 +02:00
Craig Raw d722c4972a initialise logger after configuring user defined application home 2021-01-01 10:40:08 +02:00
Bitcoin Hodler e93df8273b Fix grammar errors in README
"It's" means "it is".
2020-12-27 19:50:41 +00:00
Craig Raw a1c65cff75 initial commit of bwt integration 2020-12-20 12:34:36 +02:00
Craig Raw 18bc7bf302 improve debug logging for wallet history updates 2020-12-17 08:38:41 +02:00
Craig Raw 17bb442c78 ensure socket inputstream is buffered 2020-12-17 08:37:26 +02:00
Craig Raw 3134a9ad42 reduce time to check broadcasted tx 2020-12-16 12:37:38 +02:00
Craig Raw eda96af607 shorten details link to prevent scrollbars appearing 2020-12-16 12:10:01 +02:00
Craig Raw 038069f6e6 add scheduled service to check mempool after broadcast 2020-12-15 10:29:13 +02:00
Craig Raw ee6d8028f8 v0.9.9 2020-12-10 08:59:31 +02:00
Craig Raw 5d404f8c30 reset transaction menu shortcuts 2020-12-10 08:54:08 +02:00
Craig Raw 94662922a8 add keyboard shortcuts, fix non-mac menu exit 2020-12-10 08:38:55 +02:00
Craig Raw 582065e7f0 add specter diy keystore import 2020-12-09 17:16:44 +02:00
Craig Raw ccead92388 add view option to hide used empty addresses 2020-12-09 16:06:30 +02:00
Craig Raw 2b47740539 dont post open wallets event until scene set 2020-12-09 12:10:34 +02:00
Craig Raw 668dd88436 fix initial theme selection 2020-12-09 08:08:37 +02:00
Craig Raw 76820377ae change theme on all app windows 2020-12-08 17:24:22 +02:00
Craig Raw 6a1c3fa3da support opening wallets in new windows 2020-12-08 12:32:59 +02:00
Craig Raw dfa781e16c subscription handling for multiple app windows 2020-12-07 14:12:11 +02:00
Craig Raw 722fd84ec1 cancel running services before restart 2020-12-07 10:55:29 +02:00
Craig Raw 39d1e686f4 refactor singleton services from appcontroller to appservices 2020-12-04 15:42:17 +02:00
Craig Raw 7461b47466 move show transaction hex setting into config 2020-12-04 10:10:17 +02:00
Craig Raw 572e451279 freeze and unfreeze utxos 2020-12-01 12:45:26 +02:00
Craig Raw 88dfde3c67 import address labels from electrum wallets 2020-12-01 10:17:29 +02:00
Craig Raw 64c3db3656 reduce min height to fit 13" mbp 2020-11-29 09:21:01 +02:00
Craig Raw f0ee99b78b more efficient chart node selection 2020-11-26 15:27:25 +02:00
Craig Raw 48336bb894 add yourkit profiler credit 2020-11-26 13:42:58 +02:00
Craig Raw c804737563 upgrade to hwi 1.2.1 2020-11-26 07:56:40 +02:00
Craig Raw d395c7897e v0.9.8 2020-11-26 07:19:47 +02:00
Craig Raw 6e2e5d7a9c fix cross-platform issues 2020-11-25 11:45:26 +02:00
Craig Raw 99a39aa83f reduce unconfirmed opacity 2020-11-25 09:56:31 +02:00
Craig Raw 2ca8b91283 add increase fee functionality for rbf transactions 2020-11-24 20:34:51 +02:00
Craig Raw 94960567b5 default to mempool.space, improve mempool size tooltip display 2020-11-24 10:05:27 +02:00
Craig Raw e14349a645 only fetch external fee rates on mainnet 2020-11-23 17:34:19 +02:00
Craig Raw ee7b741a69 fetch fee rates from configurable external sources 2020-11-23 17:26:51 +02:00
Craig Raw 02c8415a7b use proxy for exchange rate if available, move to exchange rate to net package 2020-11-23 14:15:39 +02:00
Craig Raw f305e0bdc2 reduce mempool size x axis labelling 2020-11-23 12:23:45 +02:00
Craig Raw d8b7938a63 show returned errors on hwi enumerate 2020-11-19 12:54:12 +02:00
Craig Raw 44ac7f3c0e toggle fee rate selection on send, dont show zero total rate series 2020-11-19 12:33:33 +02:00
Craig Raw 2b55b5feb3 add mempool size fee selection 2020-11-19 10:46:19 +02:00
Craig Raw 43bf6ab265 get and store fee rate histogram 2020-11-18 09:54:34 +02:00
Craig Raw 80b7ee803b separate keystore and wallet scannable, improve cobo importing 2020-11-18 08:37:51 +02:00
Craig Raw 8e23bd64c7 add cobo vault support with legacy qr scanning 2020-11-17 13:32:07 +02:00
Craig Raw bf6fbebd9e add wallet to wallethistorystatusevent and filter updates 2020-11-13 12:44:27 +02:00
Craig Raw 0b9052dee9 refactor wallet field push down to entry 2020-11-13 10:12:19 +02:00
Craig Raw 6460cb88a5 v0.9.7 2020-11-12 08:40:20 +02:00
Craig Raw 90ac786707 slow down qr animation 2020-11-12 08:37:33 +02:00
Craig Raw c86006335b ensure wallet tab name and wallet name are synced on wallet open 2020-11-11 17:10:46 +02:00
Craig Raw a444e2af2f ensure wallet tab name and wallet name are synced on wallet open 2020-11-11 16:31:58 +02:00
Craig Raw bcd2c966a5 add recursive lookup for tab header 2020-11-11 14:48:10 +02:00
Craig Raw f826c2d423 upgrade to hummingbird 1.4 to fix crypto-account 2020-11-11 14:04:08 +02:00
Craig Raw 37bb1c5d97 import descriptor from qr 2020-11-11 12:26:36 +02:00
Craig Raw 68da8e6027 add support for UR registry types crypto-address, crypto-hdkey, crypto-output and crypto-account 2020-11-11 09:51:50 +02:00
Craig Raw 87fe0accd5 Use broader matching on HWI wallet types (eg bitbox02_btconly) 2020-11-05 16:46:01 +02:00
Craig Raw 8d413e839c handle server returning history with the same tx at multiple heights (electrs issue #316) 2020-11-05 16:29:15 +02:00
Craig Raw 1392199f5c add tx tree icons and input descriptions when signing 2020-11-05 10:31:54 +02:00
Craig Raw dc33b64a9c further payjoin tweaks #2 2020-11-04 16:33:05 +02:00
Craig Raw 1b220c72ea further payjoin tweaks 2020-11-04 16:32:17 +02:00
Craig Raw bcf6f77340 improve wallet import error messaging, handle p2sh-p2wpkh payjoin 2020-11-04 12:10:40 +02:00
Craig Raw 0aac8bbea7 indicate payjoin input on transaction diagram 2020-11-03 15:00:34 +02:00
Craig Raw e1acaa8a78 add payjoin (bip78) support 2020-11-03 13:54:18 +02:00
Craig Raw 13a486597c Remove illegal / characters from output descriptors 2020-11-02 07:49:55 +02:00
Craig Raw 389ce2180d accept bip21 uri in pay to field 2020-10-29 11:45:10 +02:00
Craig Raw a7a3ad012c add stacktrace on null node 2020-10-29 11:24:56 +02:00
Craig Raw 9f5eb41d90 dont add change node if no change present 2020-10-29 11:23:50 +02:00
Craig Raw 3a4d385560 avoid casting to int when comparing longs 2020-10-27 11:11:35 +02:00
Craig Raw d3881f76b9 default to trezor t on import 2020-10-27 11:09:31 +02:00
Craig Raw be5a48ce71 handle p2sh default derivation change and other small fixes 2020-10-26 15:12:10 +02:00
Craig Raw c8cd2d1cd9 v0.9.6 2020-10-21 11:22:03 +02:00
Craig Raw 5789e1c215 request password on wallet import 2020-10-21 11:11:18 +02:00
Craig Raw f48fb610e8 show no extension files in linux 2020-10-21 09:41:49 +02:00
Craig Raw 3aeba79268 use abbreviated derivation paths 2020-10-20 12:18:23 +02:00
Craig Raw 4a030e9b6e avoid ellipsis on unit dropdown 2020-10-20 11:45:58 +02:00
Craig Raw 2cf653702c handle no prior connection for fetching min relay fee rate 2020-10-20 11:36:23 +02:00
Craig Raw ac438ec023 send to multiple recipients 2020-10-20 10:56:08 +02:00
Craig Raw ee9247c066 handle support for multiple payments in a tx 2020-10-19 11:06:59 +02:00
Craig Raw 4ea5ec6c0a upgrade to patched openimag bridj with source see https://github.com/sarxos/webcam-capture/issues/723 2020-10-19 07:39:45 +02:00
Craig Raw 63a9a798f3 order available wallets to prioritise seed or usb keystores 2020-10-16 12:55:02 +02:00
Craig Raw 194f0f5c1b add file flag fallback to set testnet on startup 2020-10-16 12:37:19 +02:00
Craig Raw bda5c1a709 upgrade hwi to 1.2.0 2020-10-16 12:20:24 +02:00
Craig Raw 32327928b5 progressive read timeout 2020-10-16 12:08:27 +02:00
Craig Raw b448927a6e electrs server robustness and tuning improvements 2020-10-15 14:38:30 +02:00
Craig Raw ff578c938e break out UR implementation into hummingbird project 2020-10-15 10:38:33 +02:00
Craig Raw 998100505d change electrum call ordering and add retry handling for better behaviour on electrs 2020-10-13 12:48:32 +02:00
Craig Raw a893b37bb0 add per request delay to non-batched rpc calls 2020-10-12 18:59:50 +02:00
Craig Raw 4c58ebf138 correct handling of nodeTransactionMap empty map references 2020-10-12 11:08:24 +02:00
Craig Raw 76e148a107 add ability to import keystores and wallets with QR, add part protocol to QR scanner 2020-10-09 12:47:43 +02:00
Craig Raw 20f5e4d8db fix cobo vault keystore import 2020-10-09 09:59:26 +02:00
Craig Raw 5cd15f21ad fix icon naming for bitbox 2020-10-08 11:25:58 +02:00
Craig Raw b50cf77f3b rename xPub to xpub 2020-10-07 13:11:46 +02:00
Craig Raw 5e8148e5b9 v0.9.5 2020-10-07 12:37:10 +02:00
Craig Raw fa5ac7917b add cobo vault singlesig and multisig file import and export 2020-10-07 12:31:58 +02:00
Craig Raw eda712e269 add comment to transaction entry equals 2020-10-06 15:10:24 +02:00
Craig Raw 3c6adafc15 tweak help copy and ui 2020-10-06 14:50:25 +02:00
Craig Raw 639e145672 use tPubs instead of xPubs on labels in testnet 2020-10-06 12:39:30 +02:00
Craig Raw eb8d66bf25 add utility buttons to import and export xpubs with qr codes, show slip132 versions 2020-10-05 17:32:30 +02:00
Craig Raw 3a885b3a28 improve open wallet error dialog message 2020-10-05 12:33:05 +02:00
Craig Raw 7e93b6b198 fix byteword list as per https://github.com/BlockchainCommons/bc-bytewords/commit/9f7ec3c73c39c9638c581beb056ed03c5a3be9ff 2020-10-05 11:39:28 +02:00
Craig Raw f949bcc1cf attempt to import wallet file if open fails 2020-10-05 10:36:27 +02:00
Craig Raw 9e5937d2fe check history on all created tx script hashes when one is updated 2020-10-02 15:18:12 +02:00
Craig Raw 82a8f08440 upgrade to javafx 15, upgrade gradle plugins, ensure reproducible builds 2020-10-01 14:09:36 +02:00
Craig Raw c806959199 add doc for updating submodules 2020-10-01 12:04:41 +02:00
Craig Raw f7fb62fcea fix on windows 2020-10-01 12:00:38 +02:00
Craig Raw ea2783e51b testnet related changes and network improvements and logging 2020-10-01 09:11:02 +02:00
Craig Raw ff7a35e68b select alternative network, support testnet on hwi, other fixes 2020-09-29 15:45:22 +02:00
Craig Raw c68cda5b2f handle support for alternative (non-mainnet) networks 2020-09-28 14:32:02 +02:00
Craig Raw 2cb667a671 shorten tooltip delay, set tooltip font size equal 2020-09-26 12:43:51 +02:00
Craig Raw eb09076604 exclude utxos from transaction diagram 2020-09-26 12:30:33 +02:00
Craig Raw 32bc9f6a99 dont refresh invalid wallets 2020-09-25 17:56:57 +02:00
Craig Raw dc8a8dd857 copy non-xpub value 2020-09-25 17:48:24 +02:00
Craig Raw d5a26fdf5d followup to prev commit to handle button disable 2020-09-25 15:37:11 +02:00
Craig Raw e48e40da0d retrieve min fee rate, adjust block target rates and dont create txes with insufficient fee rate 2020-09-25 15:22:52 +02:00
Craig Raw e878c4ea78 hide validation error on clear send 2020-09-25 14:07:16 +02:00
Craig Raw ead8fa2207 clear send details on mempool tx, edit wallet output descriptor 2020-09-25 13:43:55 +02:00
Craig Raw 9a395f5dd1 v0.9.4 2020-09-21 10:00:35 +02:00
Craig Raw a99114cc6f dynamic blockchain form updating, minor tweaks 2020-09-21 09:06:01 +02:00
Craig Raw ea36c63ebb version check: test against predefined address 2020-09-20 15:09:54 +02:00
Craig Raw 77d3c848f9 add dark theme 2020-09-20 15:04:53 +02:00
Craig Raw f22312e04f add software update check 2020-09-18 15:48:05 +02:00
Craig Raw b9db4421df import transaction labels from electrum 2020-09-18 12:53:05 +02:00
Craig Raw b7aa73e99f show wallet loading progress 2020-09-18 10:50:41 +02:00
Craig Raw 4bad46c9e3 optimize electrum server queries 2020-09-17 15:10:56 +02:00
Craig Raw 084a3c76af start with simple RPC for maximum compatibility 2020-09-15 12:07:37 +02:00
Craig Raw 2e0ca1b4fa add built in tor 2020-09-14 10:51:38 +02:00
Craig Raw 4bc446724d fix keepkey hww 2020-09-10 11:40:21 +02:00
Craig Raw 0c56f5a9a1 add change password functionality 2020-09-09 16:15:33 +02:00
Craig Raw 38b27bb091 configurable wallet gap limit 2020-09-09 14:56:50 +02:00
Craig Raw b8c3bf1bea improve wallet descriptor and add output descriptor retrieval 2020-09-09 11:18:13 +02:00
Craig Raw e65f1ef3cc export specter with checksum 2020-09-08 12:42:15 +02:00
Craig Raw f25d2618b6 descriptor change multi to sortedmulti 2020-09-08 08:31:42 +02:00
Craig Raw 0adf94bf62 initial specter wallet import and export functionality 2020-09-07 17:23:12 +02:00
Craig Raw 7d7c5e5064 Support replacing existing keystore with watch only keystore 2020-09-04 13:00:40 +02:00
Craig Raw 10e4d8843d change bip39 confirmation wording 2020-09-03 20:54:46 +02:00
Craig Raw 6e10784e49 support usb message signing 2020-09-03 20:46:44 +02:00
Craig Raw 885efd7985 support bip 137 for message signing, but handle electrum approach as well when verifying 2020-09-03 19:26:54 +02:00
Craig Raw ec9534f8ae version 0.9.3 release 2020-09-03 13:41:12 +02:00
Craig Raw 6c90a7e0ba add message signing/verifying functionality 2020-09-03 13:36:43 +02:00
Craig Raw 9dd52f120f add ur toString 2020-09-03 09:38:16 +02:00
Craig Raw 4cdffec060 fix fiat amount update on transaction update 2020-09-01 13:13:38 +02:00
Craig Raw ed9bc882b7 minor ui fix, safe unsubscribe 2020-09-01 11:33:18 +02:00
Craig Raw 5054ab9564 disable sign button if signing wallet is closed 2020-09-01 09:50:44 +02:00
Craig Raw 7751f940f2 display output send type better, handle consolidation sends 2020-09-01 08:58:47 +02:00
Craig Raw 3f68ca43c2 dont show uninitialized devices (trezor t) 2020-09-01 08:04:58 +02:00
Craig Raw 68239e9472 improve device detection and display 2020-08-31 15:55:36 +02:00
Craig Raw 10edf31e13 dont open the same wallet twice 2020-08-31 11:47:06 +02:00
Craig Raw 61c3c4a3ac use minimum fee rate found for higher block targets if present 2020-08-31 11:28:03 +02:00
Craig Raw 5d6bb5e4f9 show better descriptions on outputs based on signing wallet 2020-08-31 11:22:15 +02:00
Craig Raw b808222faa deduplicate signed keystores on signature 2020-08-31 10:14:48 +02:00
Craig Raw 4d7d51f3f8 dont show status server banner (but show height in tooltip) 2020-08-30 13:47:31 +02:00
Craig Raw 546076ea48 avoid displaying any server response in error dialogs 2020-08-30 13:29:42 +02:00
Craig Raw 2bebb0925e wallet save fixes 2020-08-30 11:24:49 +02:00
Craig Raw 47855228d3 wallet load order, dont clear history on trivial changes 2020-08-30 11:15:45 +02:00
Craig Raw a6e41e3f9d fix icon on app installer 2020-08-29 20:44:50 +02:00
Craig Raw 04722204a6 set app name on X11 2020-08-29 20:06:50 +02:00
Craig Raw 452afe4731 tweak naming 2020-08-29 18:04:27 +02:00
Craig Raw af36d7b5a5 update readme for initial release 2020-08-29 17:56:09 +02:00
Craig Raw 7e225e794f fix commit 2020-08-28 19:41:32 +02:00
Craig Raw ffce18919f Merge branch 'master' of github.com:sparrowwallet/sparrow into master 2020-08-28 19:39:34 +02:00
Craig Raw ddcabf721e dont poll when signing 2020-08-28 19:38:55 +02:00
Craig Raw 7eb4b9c61d linux build fixes 2020-08-28 16:02:08 +02:00
Craig Raw 6dca03f8b4 build tweak #2 2020-08-28 15:34:35 +02:00
Craig Raw 135c1d1719 build tweak 2020-08-28 15:29:25 +02:00
Craig Raw c9a4fb4ec4 license 2020-08-28 15:20:31 +02:00
Craig Raw 06957b12e6 build ubuntu 2020-08-28 14:42:38 +02:00
Craig Raw d1fbe94d90 welcome page tweak 2020-08-28 14:41:26 +02:00
Craig Raw 1fea119173 shorten button width 2020-08-28 13:31:27 +02:00
Craig Raw 1ac264600a set linux path explicitly 2020-08-28 12:17:47 +02:00
Craig Raw 00db59c44f fix pixel stretching issue 2020-08-28 09:49:26 +02:00
Craig Raw 2c27f702bf add import 2020-08-27 17:41:17 +02:00
Craig Raw 3c2dfed96d locale and other windows related fixes 2020-08-27 17:16:20 +02:00
Craig Raw eb23cc8e61 osx only run options 2020-08-27 15:23:42 +02:00
Craig Raw fa0c74f06b set monospace font explicitly, add stage icons 2020-08-27 13:11:23 +02:00
Craig Raw ba017b2fcb windows fixes 2020-08-25 19:30:19 +02:00
Craig Raw 0a094332d2 fix folder name 2020-08-25 17:24:22 +02:00
Craig Raw 2cca6049be create installers, fix about border 2020-08-25 17:18:45 +02:00
Craig Raw 5be1f066df use matrix 2020-08-25 16:20:45 +02:00
Craig Raw d519c033ac bump version, cross platform builds 2020-08-25 16:10:56 +02:00
Craig Raw f8d15f73f6 platform specific changes 2020-08-25 15:31:04 +02:00
Craig Raw 350bddac84 java versioning 2020-08-25 12:17:33 +02:00
Craig Raw d1326cb621 update java point release 2020-08-25 09:17:28 +02:00
Craig Raw ae0e5894ca fix submodule url 2020-08-25 07:50:02 +02:00
Craig Raw 1a931c72bd checkout submodules 2020-08-24 17:46:28 +02:00
Craig Raw 92db6d9396 remove newline 2020-08-24 15:54:54 +02:00
Craig Raw 5fd59cc68a fix event name #2 2020-08-24 15:40:44 +02:00
Craig Raw ea9a310b22 fix event name 2020-08-24 15:39:31 +02:00
Craig Raw ee22d4a81d osx github action initial commit 2020-08-24 15:37:35 +02:00
Craig Raw c2fb6bcfee build, versioning and cross-platform about dlg 2020-08-24 14:52:05 +02:00
Craig Raw 8cde670efd log all errors to error log 2020-08-24 12:54:11 +02:00
Craig Raw f95cb67912 add coldcard single sig wallet import 2020-08-24 12:05:32 +02:00
Craig Raw 750f6483cb show alternative xpub values as tooltips 2020-08-24 10:12:16 +02:00
Craig Raw 53a590140d spend utxo improvements 2020-08-23 15:47:43 +02:00
Craig Raw 189ac75467 set null date on unconfirmed txes, dont let external utxos show as spendable 2020-08-22 17:20:43 +02:00
332 changed files with 15388 additions and 4200 deletions
+47
View File
@@ -0,0 +1,47 @@
name: Package
on: workflow_dispatch
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 15.0.2
if: ${{ runner.os == 'Linux' }}
uses: joschi/setup-jdk@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
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build with Gradle
run: ./gradlew jpackage
- name: Package zip distribution
if: ${{ runner.os == 'Windows' }}
run: ./gradlew packageZipDistribution
- name: Package tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: Sparrow Build - ${{ runner.os }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:craigraw/drongo.git
url = git@github.com:sparrowwallet/drongo.git
+174 -16
View File
@@ -1,19 +1,177 @@
Copyright (c) 2020 Craig Raw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-19
View File
@@ -1,19 +0,0 @@
BSD-2-Clause Plus Patent License
SPDX-License-Identifier: [BSD-2-Clause-Patent](https://spdx.org/licenses/BSD-2-Clause-Patent.html)
Copyright © 2020 Blockchain Commons, LLC
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or
(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise.
DISCLAIMER
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+81 -7
View File
@@ -1,11 +1,85 @@
# Sparrow
Bitcoin Wallet & Transaction Editor
# Sparrow Bitcoin Wallet
To clone this project, use `git clone --recursive git@github.com:craigraw/sparrow.git`
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
## Various ways to hex dump a file without spaces:
`xxd -p file | tr -d '\n'`
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
`hexdump -ve '1/1 "%.2x"'`
![Sparrow Wallet](https://sparrowwallet.com/assets/images/control-your-sends.png)
`od -t x1 -An file | tr -d '\n '`
## Building
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
`./gradlew jpackage`
When updating to the latest HEAD
`git pull --recurse-submodules`
All jar files created are reproducible builds.
## Running
If you prefer to run Sparrow directly from source, it can be launched with
`./sparrow`
Java 14 or higher must be installed.
## Configuration
Sparrow has a number of command line options, for example to change its home folder or use testnet:
```
./sparrow -h
Usage: sparrow [options]
Options:
--dir, -d
Path to Sparrow home folder
--help, -h
Show usage
--level, -l
Set log level
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
--network, -n
Network to use
Possible Values: [mainnet, testnet, regtest, signet]
```
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
`export SPARROW_NETWORK=testnet`
A final fallback which can be useful when running the Sparrow binary is to create a file called ``network-testnet`` in the Sparrow home folder (see below) to configure the testnet network.
Note that if you are connecting to an Electrum server when using testnet, that server will need to be running on testnet configuration as well.
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
Platform | Location
-------- | --------
OSX | ~/.sparrow
Linux | ~/.sparrow
Windows | %APPDATA%/Sparrow
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
## Reporting Issues
Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab above to report an issue. If possible, look in the sparrow.log file in the configuration directory for information helpful in debugging.
## License
Sparrow is licensed under the Apache 2 software licence.
## Credit
![Yourkit](https://www.yourkit.com/images/yklogo.png)
Sparrow Wallet uses the [Yourkit Java Profiler](https://www.yourkit.com/java/profiler/) to profile and improve performance.
YourKit supports open source projects with useful tools for monitoring and profiling Java and .NET applications.
+91 -22
View File
@@ -1,23 +1,35 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.8'
id 'org.openjfx.javafxplugin' version '0.0.9'
id 'org.kordamp.gradle.jdeps' version '0.9.0'
id 'org.beryx.jlink' version '2.17.4'
id 'org.beryx.jlink' version '2.22.0'
}
group 'com.sparrowwallet'
version '1.0-SNAPSHOT'
def sparrowVersion = '1.3.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
sourceCompatibility = 1.9
group "com.sparrowwallet"
version "${sparrowVersion}"
repositories {
mavenCentral()
maven { url 'https://oss.sonatype.org/content/groups/public' }
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
maven { url 'https://jitpack.io' }
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
javafx {
version = "14"
version = "15"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -39,11 +51,14 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20200803')
implementation('com.sparrowwallet:hummingbird:1.5.5')
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('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@@ -54,6 +69,11 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.7')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
testImplementation('junit:junit:4.12')
}
@@ -62,13 +82,22 @@ mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
compileJava {
options.with {
fork = true
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"])
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"])
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/**"
}
}
}
run {
applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
@@ -81,7 +110,14 @@ run {
"--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.mac=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"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
}
jlink {
@@ -93,6 +129,7 @@ jlink {
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
@@ -111,26 +148,58 @@ jlink {
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--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"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
}
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "0.9.1"
skipInstaller = true
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX
imageOptions = []
installerOptions = [
'--file-associations', 'src/main/resources/associations.properties',
]
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
installerOptions = ['--file-associations', 'src/main/deploy/associations.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
if (org.gradle.internal.os.OperatingSystem.current().macOsX) {
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/macosx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macosx/']
imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
installerType = "dmg"
}
}
}
task removeGroupWritePermission(type: Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
}
task packageZipDistribution(type: Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip"
destinationDirectory = file("$buildDir/jpackage")
from("$buildDir/jpackage/") {
include "Sparrow/**"
}
}
task packageTarDistribution(type: Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP
from("$buildDir/jpackage/") {
include "Sparrow/**"
}
}
+1 -1
Submodule drongo updated: 446eac3483...49654b7c82
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env sh
args="$*"
args="${args%"${args##*[![:space:]]}"}"
if [ -n "$args" ]
then
./gradlew run --args="$args"
else
./gradlew run
fi
+8
View File
@@ -0,0 +1,8 @@
@echo off
set ARGS=%*
if "%ARGS%" == "" (
gradlew.bat run
) else (
gradlew.bat run --args="%ARGS%"
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.6</string>
<string>1.3.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -30,7 +30,7 @@
<key>CFBundleVersion</key>
<string>100</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C) 2020</string>
<string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUsageDescription</key>
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -1,11 +1,20 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class AboutController {
private Stage stage;
@FXML
private Label title;
public void initializeView() {
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
}
public void setStage(Stage stage) {
this.stage = stage;
}
@@ -13,4 +22,8 @@ public class AboutController {
public void close(ActionEvent event) {
stage.close();
}
public void openDonate(ActionEvent event) {
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/donate");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,670 @@
package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.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.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.net.*;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Worker;
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.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
import org.berndpruenster.netlayer.tor.Tor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
public class AppServices {
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
private static final int SERVER_PING_PERIOD_SECS = 60;
private static final int PUBLIC_SERVER_RETRY_PERIOD_SECS = 3;
private static final int ENUMERATE_HW_PERIOD_SECS = 30;
private static final int RATES_PERIOD_SECS = 5 * 60;
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 AppServices INSTANCE;
private final MainApp application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
private TrayManager trayManager;
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
private ExchangeSource.RatesService ratesService;
private ElectrumServer.ConnectionService connectionService;
private Hwi.ScheduledEnumerateService deviceEnumerateService;
private VersionCheckService versionCheckService;
private TorService torService;
private static Integer currentBlockHeight;
private static Map<Integer, Double> targetBlockFeeRates;
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
private static CurrencyRate fiatCurrencyExchangeRate;
private static List<Device> devices;
private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
if(online) {
if(Config.get().requiresInternalTor() && !isTorRunning()) {
torService.start();
} else {
restartServices();
}
} else {
connectionService.cancel();
ratesService.cancel();
versionCheckService.cancel();
}
}
};
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
}
public void start() {
Config config = Config.get();
connectionService = createConnectionService();
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
onlineProperty.addListener(onlineServicesListener);
if(config.getMode() == Mode.ONLINE) {
if(config.requiresInternalTor()) {
torService.start();
} else {
restartServices();
}
}
}
private void restartServices() {
Config config = Config.get();
if(config.hasServerAddress()) {
restartService(connectionService);
}
if(config.isFetchRates()) {
restartService(ratesService);
}
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
restartService(versionCheckService);
}
}
private void restartService(ScheduledService<?> service) {
if(service.isRunning()) {
service.cancel();
}
if(service.getState() == Worker.State.CANCELLED || service.getState() == Worker.State.FAILED) {
service.reset();
}
if(!service.isRunning()) {
service.start();
}
}
public void stop() {
if(connectionService != null) {
connectionService.cancel();
}
if(ratesService != null) {
ratesService.cancel();
}
if(versionCheckService != null) {
versionCheckService.cancel();
}
if(Tor.getDefault() != null) {
Tor.getDefault().shutdown();
}
}
private ElectrumServer.ConnectionService createConnectionService() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
connectionService.setRestartOnFailure(true);
EventManager.get().register(connectionService);
connectionService.setOnRunning(workerStateEvent -> {
if(!ElectrumServer.isConnected()) {
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
}
});
connectionService.setOnSucceeded(successEvent -> {
connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
connectionService.setRestartOnFailure(true);
onlineProperty.removeListener(onlineServicesListener);
onlineProperty.setValue(true);
onlineProperty.addListener(onlineServicesListener);
if(connectionService.getValue() != null) {
EventManager.get().post(connectionService.getValue());
}
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
connectionService.resetConnection();
if(failEvent.getSource().getException() instanceof ServerConfigException) {
connectionService.setRestartOnFailure(false);
}
if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) {
TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException();
connectionService.setRestartOnFailure(false);
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
File crtFile = Config.get().getElectrumServerCert();
if(crtFile != null) {
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.");
} else {
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
if(crtFile != null) {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
crtFile.delete();
Platform.runLater(() -> restartService(connectionService));
return;
}
}
}
}
}
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().requiresTor()) {
Config.get().setUseProxy(false);
Platform.runLater(() -> restartService(torService));
return;
}
onlineProperty.removeListener(onlineServicesListener);
onlineProperty.setValue(false);
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())));
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
}
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
return connectionService;
}
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true);
ratesService.setOnSucceeded(successEvent -> {
EventManager.get().post(ratesService.getValue());
});
return ratesService;
}
private VersionCheckService createVersionCheckService() {
VersionCheckService versionCheckService = new VersionCheckService();
versionCheckService.setDelay(Duration.seconds(10));
versionCheckService.setPeriod(Duration.hours(VERSION_CHECK_PERIOD_HOURS));
versionCheckService.setRestartOnFailure(true);
versionCheckService.setOnSucceeded(successEvent -> {
VersionUpdatedEvent event = versionCheckService.getValue();
if(event != null) {
EventManager.get().post(event);
}
});
return versionCheckService;
}
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS));
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
//Null devices are returned if the app is currently prompting for a pin. Otherwise, the enumerate clears the pin screen
if(devices != null) {
//If another instance of HWI is currently accessing the usb interface, HWI returns empty device models. Ignore this run if that happens
List<Device> validDevices = devices.stream().filter(device -> device.getModel() != null).collect(Collectors.toList());
if(validDevices.size() == devices.size()) {
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
}
}
});
return enumerateService;
}
private TorService createTorService() {
TorService torService = new TorService();
torService.setPeriod(Duration.hours(1000));
torService.setRestartOnFailure(true);
torService.setOnRunning(workerStateEvent -> {
EventManager.get().post(new TorBootStatusEvent());
});
torService.setOnSucceeded(workerStateEvent -> {
Tor.setDefault(torService.getValue());
torService.cancel();
restartServices();
EventManager.get().post(new TorReadyStatusEvent());
});
torService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
if(exception instanceof TorServerAlreadyBoundException) {
String proxyServer = Config.get().getProxyServer();
if(proxyServer == null || proxyServer.equals("")) {
proxyServer = "localhost:9050";
Config.get().setProxyServer(proxyServer);
}
if(proxyServer.equals("localhost:9050") || proxyServer.equals("127.0.0.1:9050")) {
Config.get().setUseProxy(true);
torService.cancel();
restartServices();
EventManager.get().post(new TorExternalStatusEvent());
return;
}
}
EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException()));
});
return torService;
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
public static Proxy getProxy() {
Config config = Config.get();
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);
} else if(AppServices.isTorRunning()) {
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
}
return null;
}
static void initialize(MainApp application) {
INSTANCE = new AppServices(application);
}
public static AppServices get() {
return INSTANCE;
}
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
Parent root = appLoader.load();
AppController appController = appLoader.getController();
Scene scene = new Scene(root);
scene.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
stage.setTitle("Sparrow");
stage.setMinWidth(650);
stage.setMinHeight(730);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
appController.initializeView();
stage.show();
return appController;
} catch(IOException e) {
log.error("Could not load app FXML", e);
throw new IllegalStateException(e);
}
}
public static boolean isReducedWindowHeight(Node node) {
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
}
public MainApp getApplication() {
return application;
}
public void minimizeStage(Stage stage) {
if(trayManager == null) {
trayManager = new TrayManager();
}
trayManager.addStage(stage);
stage.hide();
}
public Map<Wallet, Storage> getOpenWallets() {
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
for(WalletTabData walletTabData : walletTabDataList) {
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
}
}
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();
return optWindow.orElse(null);
}
public Window getWindowForPSBT(PSBT psbt) {
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWallet().canSign(psbt))).map(Map.Entry::getKey).findFirst();
return optWindow.orElse(null);
}
public double getWalletWindowMaxX() {
return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d);
}
public static boolean isConnecting() {
return get().connectionService != null && get().connectionService.isConnecting();
}
public static boolean isConnected() {
return onlineProperty.get() && get().connectionService != null && get().connectionService.isConnected();
}
public static BooleanProperty onlineProperty() {
return onlineProperty;
}
public static Integer getCurrentBlockHeight() {
return currentBlockHeight;
}
public static Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
return mempoolHistogram;
}
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
if(mempoolHistogram.isEmpty()) {
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
}
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
}
public static Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
}
public static CurrencyRate getFiatCurrencyExchangeRate() {
return fiatCurrencyExchangeRate;
}
public static List<Device> getDevices() {
return devices == null ? new ArrayList<>() : devices;
}
public static BitcoinURI getPayjoinURI(Address address) {
return payjoinURIs.get(address);
}
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null) {
throw new IllegalArgumentException("Not a payjoin URI");
}
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons);
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
Alert alert = new Alert(alertType, content, buttons);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
return alert.showAndWait();
}
public static void setStageIcon(Window window) {
Stage stage = (Stage)window;
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow-large.png")));
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
}
}
public static Font getMonospaceFont() {
return Font.font("Roboto Mono", 13);
}
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
}
@Subscribe
public void usbDevicesFound(UsbDeviceEvent event) {
devices = Collections.unmodifiableList(event.getDevices());
}
@Subscribe
public void newBlock(NewBlockEvent event) {
currentBlockHeight = event.getHeight();
String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status));
}
@Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
}
@Subscribe
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
feeRatesService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(feeRatesService.getValue());
});
//Perform once-off fee rates retrieval to immediately change displayed rates
feeRatesService.start();
}
@Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
if(ratesService != null) {
ratesService.cancel();
if(Config.get().getMode() != Mode.OFFLINE && event.getExchangeSource() != ExchangeSource.NONE) {
ratesService = createRatesService(event.getExchangeSource(), event.getCurrency());
ratesService.start();
}
}
}
@Subscribe
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
fiatCurrencyExchangeRate = event.getCurrencyRate();
}
@Subscribe
public void versionCheckStatus(VersionCheckStatusEvent event) {
versionCheckService.cancel();
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled() && Network.get() == Network.MAINNET) {
versionCheckService = createVersionCheckService();
versionCheckService.start();
}
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
if(event.getWalletTabDataList().isEmpty()) {
walletWindows.remove(event.getWindow());
} else {
walletWindows.put(event.getWindow(), event.getWalletTabDataList());
}
List<WalletTabData> allWallets = walletWindows.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
Platform.runLater(() -> {
if(!Window.getWindows().isEmpty()) {
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
}
});
boolean usbWallet = false;
for(WalletTabData walletTabData : allWallets) {
Wallet wallet = walletTabData.getWallet();
Storage storage = walletTabData.getStorage();
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
usbWallet = true;
if(deviceEnumerateService == null) {
deviceEnumerateService = createDeviceEnumerateService();
}
if(deviceEnumerateService.getState() == Worker.State.CANCELLED) {
deviceEnumerateService.reset();
}
if(!deviceEnumerateService.isRunning()) {
deviceEnumerateService.start();
}
break;
}
}
if(!usbWallet && deviceEnumerateService != null && deviceEnumerateService.isRunning()) {
deviceEnumerateService.cancel();
EventManager.get().post(new UsbDeviceEvent(Collections.emptyList()));
}
}
@Subscribe
public void requestConnect(RequestConnectEvent event) {
onlineProperty.set(true);
}
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
onlineProperty.set(false);
}
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
restartBwt(event.getWallet());
}
@Subscribe
public void walletOpening(WalletOpeningEvent event) {
restartBwt(event.getWallet());
}
private void restartBwt(Wallet wallet) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
connectionService.cancel();
}
}
@Subscribe
public void bwtShutdown(BwtShutdownEvent event) {
if(onlineProperty().get() && !connectionService.isRunning()) {
connectionService.reset();
connectionService.start();
}
}
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.Parameter;
import com.sparrowwallet.drongo.Network;
import org.slf4j.event.Level;
public class Args {
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
public String dir;
@Parameter(names = { "--network", "-n" }, description = "Network to use")
public Network network;
@Parameter(names = { "--level", "-l" }, description = "Set log level")
public Level level;
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
public boolean help;
}
@@ -1,6 +1,9 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.DescriptorArea;
import com.sparrowwallet.sparrow.control.ScriptArea;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
@@ -39,4 +42,42 @@ public abstract class BaseController {
protected String describeScriptChunk(ScriptChunk chunk) {
return chunk.toString();
}
protected void initializeDescriptorField(DescriptorArea descriptorArea) {
Popup popup = new Popup();
Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg);
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeKeystore(hoverKeystore));
popup.show(descriptorArea, pos.getX(), pos.getY() + 10);
}
});
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide();
});
}
protected String describeKeystore(Keystore keystore) {
if(keystore.isValid()) {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append("/");
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
builder.append("]");
builder.append(keystore.getExtendedPublicKey().toString());
return builder.toString();
}
return "Invalid";
}
}
@@ -1,70 +1,92 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.sparrow.control.WelcomeDialog;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Network;
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 javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.File;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.3.2";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
private Stage mainStage;
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
super.init();
}
@Override
public void start(Stage stage) throws Exception {
this.mainStage = stage;
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
AppServices.initialize(this);
boolean createNewWallet = false;
Mode mode = Config.get().getMode();
if(mode == null) {
WelcomeDialog welcomeDialog = new WelcomeDialog(getHostServices());
WelcomeDialog welcomeDialog = new WelcomeDialog();
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
Config.get().setCoreWallet(Bwt.DEFAULT_CORE_WALLET);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)].getUrl());
}
}
}
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
Parent root = transactionLoader.load();
AppController appController = transactionLoader.getController();
appController.setApplication(this);
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE && Config.get().getCoreWallet() == null) {
Config.get().setCoreMultiWallet(Boolean.TRUE);
Config.get().setCoreWallet("");
}
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm());
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
Config.get().setHdCapture(Boolean.TRUE);
}
stage.setTitle("Sparrow");
stage.setMinWidth(650);
stage.setMinHeight(700);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
appController.initializeView();
stage.show();
AppController appController = AppServices.newAppWindow(stage);
if(createNewWallet) {
appController.newWallet(null);
@@ -72,20 +94,80 @@ public class MainApp extends Application {
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
for(File walletFile : recentWalletFiles) {
//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);
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
Platform.runLater(() -> appController.openWalletFile(walletFile));
appController.openWalletFile(walletFile, false);
}
}
}
AppServices.get().start();
}
@Override
public void stop() throws Exception {
AppServices.get().stop();
mainStage.close();
}
public static void main(String[] args) {
launch(args);
public static void main(String[] argv) {
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase()).acceptUnknownOptions(true).build();
jCommander.parse(argv);
if(args.help) {
jCommander.usage();
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
}
if(args.dir != null) {
System.setProperty(APP_HOME_PROPERTY, args.dir);
getLogger().info("Using configured Sparrow home folder of " + args.dir);
}
if(args.network != null) {
Network.set(args.network);
} else {
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
if(envNetwork != null) {
try {
Network.set(Network.valueOf(envNetwork.toUpperCase()));
} catch(Exception e) {
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
}
}
}
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
if(testnetFlag.exists()) {
Network.set(Network.TESTNET);
}
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);
}
if(Network.get() != Network.MAINNET) {
getLogger().info("Using " + Network.get() + " configuration");
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
}
private static Logger getLogger() {
return LoggerFactory.getLogger(MainApp.class);
}
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow;
import javafx.application.Preloader;
import javafx.stage.Stage;
public class MainAppPreloader extends Preloader {
@Override
public void start(Stage stage) {
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
}
}
@@ -1,11 +1,7 @@
package com.sparrowwallet.sparrow;
import java.io.File;
public class TabData {
private TabType type;
private File file;
private String text;
private final TabType type;
public TabData(TabType type) {
this.type = type;
@@ -15,22 +11,6 @@ public class TabData {
return type;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public enum TabType {
WALLET, TRANSACTION
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Theme {
LIGHT, DARK
}
@@ -0,0 +1,12 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -4,14 +4,22 @@ import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import java.io.File;
public class TransactionTabData extends TabData {
private final File file;
private final TransactionData transactionData;
public TransactionTabData(TabType type, TransactionData transactionData) {
public TransactionTabData(TabType type, File file, TransactionData transactionData) {
super(type);
this.file = file;
this.transactionData = transactionData;
}
public File getFile() {
return file;
}
public TransactionData getTransactionData() {
return transactionData;
}
@@ -0,0 +1,130 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.controlsfx.control.StatusBar;
public class WelcomeController {
@FXML
private VBox welcomeBox;
@FXML
private VBox step1;
@FXML
private VBox step2;
@FXML
private VBox step3;
@FXML
private VBox step4;
@FXML
private StatusBar serverStatus;
@FXML
private UnlabeledToggleSwitch serverToggle;
public void initializeView() {
step1.managedProperty().bind(step1.visibleProperty());
step2.managedProperty().bind(step2.visibleProperty());
step3.managedProperty().bind(step3.visibleProperty());
step4.managedProperty().bind(step4.visibleProperty());
step2.setVisible(false);
step3.setVisible(false);
step4.setVisible(false);
welcomeBox.getStyleClass().add("offline");
serverStatus.setText("Offline");
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
});
}
public boolean next() {
if(step1.isVisible()) {
step1.setVisible(false);
step2.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
});
wait.play();
return true;
}
if(step2.isVisible()) {
step2.setVisible(false);
step3.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
if(step3.isVisible()) {
step3.setVisible(false);
step4.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("private-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
}
return false;
}
public boolean back() {
if(step2.isVisible()) {
step2.setVisible(false);
step1.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("offline");
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(false);
serverStatus.setText("Offline");
});
wait.play();
return false;
}
if(step3.isVisible()) {
step3.setVisible(false);
step2.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
return true;
}
if(step4.isVisible()) {
step4.setVisible(false);
step3.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
return false;
}
}
@@ -0,0 +1,69 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.io.IOException;
public class WelcomeDialog extends Dialog<Mode> {
public WelcomeDialog() {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
try {
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
dialogPane.setContent(welcomeLoader.load());
WelcomeController welcomeController = welcomeLoader.getController();
welcomeController.initializeView();
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
Button onlineButton = (Button)dialogPane.lookupButton(onlineButtonType);
Button offlineButton = (Button)dialogPane.lookupButton(offlineButtonType);
nextButton.managedProperty().bind(nextButton.visibleProperty());
backButton.managedProperty().bind(backButton.visibleProperty());
onlineButton.managedProperty().bind(onlineButton.visibleProperty());
offlineButton.managedProperty().bind(offlineButton.visibleProperty());
backButton.setDisable(true);
onlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
offlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
if(!welcomeController.next()) {
nextButton.setVisible(false);
onlineButton.setDefaultButton(true);
}
backButton.setDisable(false);
event.consume();
});
backButton.addEventFilter(ActionEvent.ACTION, event -> {
nextButton.setVisible(true);
if(!welcomeController.back()) {
backButton.setDisable(true);
}
event.consume();
});
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -32,7 +32,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
setTooltip(tooltip);
@@ -58,7 +58,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath() + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
@@ -10,10 +11,10 @@ import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Font;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
public class AddressTreeTable extends CoinTreeTable {
public void initialize(NodeEntry rootEntry) {
@@ -33,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
getColumns().add(addressCol);
if(address != null) {
addressCol.setMinWidth(TextUtils.computeTextWidth(Font.font("Courier"), address, 0.0));
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
@@ -60,7 +61,10 @@ public class AddressTreeTable extends CoinTreeTable {
Integer highestUsedIndex = rootEntry.getNode().getHighestUsedIndex();
if(highestUsedIndex != null) {
scrollTo(highestUsedIndex);
OptionalInt tableIndex = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().getIndex() == highestUsedIndex + 1).mapToInt(childEntry -> rootEntry.getChildren().indexOf(childEntry)).findFirst();
if(tableIndex.isPresent() && tableIndex.getAsInt() > 5) {
scrollTo(tableIndex.getAsInt());
}
}
setOnMouseClicked(mouseEvent -> {
@@ -39,6 +39,7 @@ public class BalanceChart extends LineChart<Number, Number> {
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
.map(entry -> (TransactionEntry)entry)
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
.collect(Collectors.toList());
@@ -81,9 +82,9 @@ public class BalanceChart extends LineChart<Number, Number> {
for(int i = 0; i < balanceSeries.getData().size(); i++) {
XYChart.Data<Number, Number> data = balanceSeries.getData().get(i);
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
if(data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
if(transactionEntry.getBlockTransaction().getDate() != null && data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
symbol.getStyleClass().add("selected");
selectedEntry = transactionEntry;
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
@@ -9,11 +10,11 @@ import javafx.scene.chart.XYChart;
import java.util.Iterator;
import java.util.Map;
public class FeeRatesChart extends LineChart<String, Number> {
public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
private XYChart.Series<String, Number> feeRateSeries;
private Integer selectedTargetBlocks;
public FeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
public BlockTargetFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
@@ -27,9 +28,11 @@ public class FeeRatesChart extends LineChart<String, Number> {
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
Integer targetBlocks = targetBlocksIter.next();
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
}
}
if(selectedTargetBlocks != null) {
@@ -58,7 +58,7 @@ public class CoinLabel extends CopyableLabel {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats";
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
BitcoinUnit unit = bitcoinUnit;
@@ -4,13 +4,15 @@ import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public CoinTextFormatter() {
super(new CoinFilter());
@@ -2,9 +2,27 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
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.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;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.StackPane;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit;
@@ -33,4 +51,61 @@ public class CoinTreeTable extends TreeTableView<Entry> {
refresh();
}
}
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
if(getRoot() != null) {
Entry entry = getRoot().getValue();
if(entry != null && event.getWallet() != null && entry.getWallet() == event.getWallet()) {
Platform.runLater(() -> {
if(event.getErrorMessage() != null) {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
} else {
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
}
});
}
}
}
protected Node getDefaultPlaceholder(Wallet wallet) {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.setBirthDate(optDate.get());
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
}
});
if(wallet.getBirthDate() == null) {
hyperlink.setText("Scan for previous transactions?");
} else {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?");
}
stackPane.getChildren().add(hyperlink);
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
}
@@ -33,9 +33,14 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
} else {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
}
Tooltip tooltip = new Tooltip();
int height = utxoEntry.getHashIndex().getHeight();
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
@@ -0,0 +1,29 @@
package com.sparrowwallet.sparrow.control;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateStringConverter extends StringConverter<LocalDate> {
public static final String FORMAT_PATTERN = "yyyy/MM/dd";
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN);
@Override
public String toString(LocalDate date) {
if (date != null) {
return DATE_FORMATTER.format(date);
} else {
return "";
}
}
@Override
public LocalDate fromString(String string) {
if (string != null && !string.isEmpty()) {
return LocalDate.parse(string, DATE_FORMATTER);
} else {
return null;
}
}
}
@@ -0,0 +1,89 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
private Wallet wallet;
public void setWallet(Wallet wallet) {
clear();
this.wallet = wallet;
DescriptorContextMenu contextMenu = new DescriptorContextMenu(wallet, this);
setContextMenu(contextMenu);
PolicyType policyType = wallet.getPolicyType();
ScriptType scriptType = wallet.getScriptType();
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
for(Keystore keystore : keystores) {
append(",", "descriptor-text");
replace(getLength(), getLength(), keystore.getScriptName(), List.of(keystore.isValid() ? "descriptor-text" : "descriptor-error", keystore.getScriptName()));
}
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
}
public Wallet getWallet() {
return wallet;
}
public void clear() {
super.clear();
this.wallet = null;
setDisable(false);
setContextMenu(null);
}
private static class DescriptorContextMenu extends ContextMenu {
public DescriptorContextMenu(Wallet wallet, DescriptorArea descriptorArea) {
MenuItem copyvalue = new MenuItem("Copy Value");
copyvalue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(descriptorArea.getText());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyvalue);
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyOutputDescriptor);
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
}
}
}
@@ -1,22 +1,22 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final KeyDerivation keyDerivation;
private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(List<Device> devices, Wallet wallet, KeyDerivation keyDerivation) {
super(devices);
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet;
this.keyDerivation = keyDerivation;
this.outputDescriptor = outputDescriptor;
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -26,7 +26,7 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, keyDerivation, device);
return new DevicePane(wallet, outputDescriptor, device);
}
@Subscribe
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
@@ -15,10 +15,12 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class DeviceDialog<R> extends Dialog<R> {
private final List<Device> operationDevices;
private final List<String> operationFingerprints;
private final Accordion deviceAccordion;
private final VBox scanBox;
private final Label scanLabel;
@@ -27,11 +29,12 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
this(null);
}
public DeviceDialog(List<Device> operationDevices) {
this.operationDevices = operationDevices;
public DeviceDialog(List<String> operationFingerprints) {
this.operationFingerprints = operationFingerprints;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -64,7 +67,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppController.getDevices();
List<Device> devices = AppServices.getDevices();
if(devices == null || devices.isEmpty()) {
scanBox.setVisible(true);
} else {
@@ -86,6 +89,8 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
}
private void scan() {
@@ -96,6 +101,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
deviceAccordion.getPanes().clear();
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
@@ -103,16 +109,20 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
}
protected void setDevices(List<Device> devices) {
List<Device> dialogDevices = devices;
if(operationDevices != null && dialogDevices.containsAll(operationDevices)) {
dialogDevices = operationDevices;
List<Device> dialogDevices = new ArrayList<>(devices);
dialogDevices.removeIf(Objects::isNull);
if(operationFingerprints != null) {
dialogDevices.removeIf(device -> {
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.getNeedsPinSent() || device.getNeedsPassphraseSent());
});
}
deviceAccordion.getPanes().clear();
if(dialogDevices.isEmpty()) {
scanBox.setVisible(true);
scanLabel.setText("No devices found");
scanLabel.setText("No matching devices found");
} else {
scanBox.setVisible(false);
for(Device device : dialogDevices) {
@@ -2,19 +2,21 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -22,21 +24,28 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
private final DeviceOperation deviceOperation;
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final Device device;
private CustomPasswordField pinField;
@@ -46,6 +55,7 @@ public class DevicePane extends TitledDescriptionPane {
private SplitMenuButton importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
@@ -54,7 +64,9 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -63,13 +75,7 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createImportButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
@@ -79,7 +85,9 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -88,23 +96,19 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createSignButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) {
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -113,15 +117,45 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createDisplayAddressButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.device = device;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createSignMessageButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
private void initialise(Device device) {
if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else if(device.getError() != null) {
setError("Error", device.getError());
Platform.runLater(() -> {
setExpanded(true);
});
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
@Override
@@ -163,17 +197,31 @@ public class DevicePane extends TitledDescriptionPane {
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation());
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
});
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"};
for(int i = 0; i < accounts.length; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
@@ -203,13 +251,30 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
displayAddressButton.setDisable(true);
}
}
private void createSignMessageButton() {
signMessageButton = new Button("Sign Message");
signMessageButton.setDefaultButton(true);
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
signMessageButton.setOnAction(event -> {
signMessageButton.setDisable(true);
signMessage();
});
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
signMessageButton.setDisable(true);
}
}
private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
}
}
@@ -256,16 +321,26 @@ public class DevicePane extends TitledDescriptionPane {
}
private Node getPassphraseEntry() {
CustomTextField passphraseField = (CustomTextField)TextFields.createClearableTextField();
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
Button sendPassphraseButton = new Button("Send Passphrase");
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
sendPassphraseButton.setText("Send Passphrase");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
sendPassphrase(passphrase.get());
});
MenuItem removePassphrase = new MenuItem("Toggle Passphrase Off");
removePassphrase.setOnAction(event -> {
setExpanded(false);
setDescription("Toggling passphrase off, check device...");
togglePassphraseOff();
});
sendPassphraseButton.getItems().add(removePassphrase);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
@@ -273,9 +348,40 @@ public class DevicePane extends TitledDescriptionPane {
contentBox.getChildren().add(sendPassphraseButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
Platform.runLater(passphraseField::requestFocus);
return contentBox;
}
private Node getTogglePassphraseOn() {
CopyableLabel label = new CopyableLabel("Passphrase is currently disabled");
HBox.setHgrow(label, Priority.ALWAYS);
Button togglePassphraseOn = new Button("Toggle Passphrase On");
togglePassphraseOn.setOnAction(event -> {
setExpanded(false);
hideButtons(importButton, signButton, displayAddressButton, signMessageButton);
setDescription("Toggling passphrase on, check device...");
togglePassphraseOn();
});
HBox contentBox = new HBox();
contentBox.setSpacing(20);
contentBox.setAlignment(Pos.CENTER_LEFT);
contentBox.getChildren().addAll(label, togglePassphraseOn);
contentBox.setPadding(new Insets(10, 30, 10, 30));
return contentBox;
}
private void hideButtons(Node... buttons) {
for(Node button : buttons) {
if(button != null) {
button.setVisible(false);
}
}
}
private void promptPin() {
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
promptPinService.setOnSucceeded(workerStateEvent -> {
@@ -289,7 +395,7 @@ public class DevicePane extends TitledDescriptionPane {
}
});
promptPinService.setOnFailed(workerStateEvent -> {
setError(promptPinService.getException().getMessage(), null);
setError("Error", promptPinService.getException().getMessage());
unlockButton.setDisable(false);
});
promptPinService.start();
@@ -312,6 +418,7 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(true);
} else {
showOperationButton();
setContent(getTogglePassphraseOn());
}
} else {
setError("Incorrect PIN", null);
@@ -322,7 +429,7 @@ public class DevicePane extends TitledDescriptionPane {
}
});
sendPinService.setOnFailed(workerStateEvent -> {
setError(sendPinService.getException().getMessage(), null);
setError("Error", sendPinService.getException().getMessage());
enterPinButton.setDisable(false);
});
setDescription("Unlocking...");
@@ -342,8 +449,7 @@ public class DevicePane extends TitledDescriptionPane {
if(device.getFingerprint() != null) {
setPassphraseButton.setVisible(false);
device.setNeedsPassphraseSent(false);
setDefaultStatus();
setDescription("Passphrase sent");
showOperationButton();
} else {
setError("Passphrase send failed", null);
@@ -352,13 +458,43 @@ public class DevicePane extends TitledDescriptionPane {
}
});
enumerateService.setOnFailed(workerStateEvent -> {
setError(enumerateService.getException().getMessage(), null);
setError("Error", enumerateService.getException().getMessage());
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
});
enumerateService.start();
}
private void togglePassphraseOff() {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
device.setNeedsPassphraseSent(false);
setPassphraseButton.setVisible(false);
setDescription("Unlocked");
showOperationButton();
});
togglePassphraseService.setOnFailed(workerStateEvent -> {
setError("Error", togglePassphraseService.getException().getMessage());
});
togglePassphraseService.start();
}
private void togglePassphraseOn() {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
device.setNeedsPassphraseSent(true);
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setDescription("Enter passphrase");
setContent(getPassphraseEntry());
setExpanded(true);
});
togglePassphraseService.setOnFailed(workerStateEvent -> {
setError("Error", togglePassphraseService.getException().getMessage());
});
togglePassphraseService.start();
}
private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
@@ -373,7 +509,7 @@ public class DevicePane extends TitledDescriptionPane {
importXpub(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError(enumerateService.getException().getMessage(), null);
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
@@ -389,14 +525,29 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
EventManager.get().post(new KeystoreImportEvent(keystore));
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);
wallet.setName(device.getModel().toDisplayString());
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));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
});
getXpubService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubService.getException().getMessage());
@@ -415,6 +566,7 @@ public class DevicePane extends TitledDescriptionPane {
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage());
signButton.setDisable(false);
});
setDescription("Signing...");
@@ -423,7 +575,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath());
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
@@ -436,18 +588,35 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressService.start();
}
private void signMessage() {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
signMessageService.setOnFailed(failedEvent -> {
setError("Could not sign message", signMessageService.getException().getMessage());
signMessageButton.setDisable(false);
});
setDescription("Signing message...");
signMessageService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@@ -487,7 +656,11 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox;
}
public Device getDevice() {
return device;
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
}
}
@@ -11,8 +11,8 @@ import java.util.List;
public class DeviceSignDialog extends DeviceDialog<PSBT> {
private final PSBT psbt;
public DeviceSignDialog(List<Device> devices, PSBT psbt) {
super(devices);
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
super(operationFingerprints);
this.psbt = psbt;
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceSignMessageDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final String message;
private final KeyDerivation keyDerivation;
public DeviceSignMessageDialog(List<String> operationFingerprints, Wallet wallet, String message, KeyDerivation keyDerivation) {
super(operationFingerprints);
this.wallet = wallet;
this.message = message;
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, message, keyDerivation, device);
}
@Subscribe
public void messageSigned(MessageSignedEvent event) {
if(wallet == event.getWallet()) {
setResult(event.getSignature());
}
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.control;
import tornadofx.control.Form;
public class DynamicForm extends Form {
private DynamicUpdate dynamicUpdate;
public DynamicForm() {
super();
}
public void setDynamicUpdate(DynamicUpdate dynamicUpdate) {
this.dynamicUpdate = dynamicUpdate;
}
@Override
protected void layoutChildren() {
if(dynamicUpdate != null) {
dynamicUpdate.update();
}
super.layoutChildren();
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.control;
public interface DynamicUpdate {
void update();
}
@@ -1,8 +1,12 @@
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.wallet.BlockTransaction;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -15,14 +19,21 @@ import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
class EntryCell extends TreeTableCell<Entry, Entry> {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public class EntryCell extends TreeTableCell<Entry, Entry> {
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
public EntryCell() {
super();
@@ -43,31 +54,56 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
} else {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
setText("Unconfirmed");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else {
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
}
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip);
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.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
});
setGraphic(viewTransactionButton);
actionBox.getChildren().add(viewTransactionButton);
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.setOnAction(event -> {
increaseFee(transactionEntry);
});
actionBox.getChildren().add(increaseFeeButton);
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
@@ -76,11 +112,27 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
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)) {
Button signMessageButton = new Button("");
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12);
signMessageButton.setGraphic(signMessageGlyph);
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);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry));
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
@@ -91,30 +143,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
actionBox.getChildren().add(viewTransactionButton);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) {
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.setOnAction(event -> {
List<HashIndexEntry> utxoEntries = getTreeTableView().getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent())
.collect(Collectors.toList());
if(!utxoEntries.contains(hashIndexEntry)) {
utxoEntries = List.of(hashIndexEntry);
}
final List<HashIndexEntry> spendingUtxoEntries = utxoEntries;
EventManager.get().post(new SendActionEvent(utxoEntries));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries)));
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
});
actionBox.getChildren().add(spendUtxoButton);
}
@@ -124,6 +163,140 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
}
private static void increaseFee(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
Collections.shuffle(walletUtxos);
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
//If there is insufficent change output, include another random UTXO so the fee can be increased
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
utxos.add(utxo);
changeTotal += utxo.getValue();
vSize += inputSize;
}
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
externalOutputs.removeAll(ourOutputs);
externalOutputs.addAll(consolidationOutputs);
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
try {
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(label);
if(matcher.matches()) {
String base = matcher.group(1);
if(matcher.groupCount() > 2 && matcher.group(3) != null) {
int count = Integer.parseInt(matcher.group(3)) + 1;
label = base + "(Replaced By Fee #" + count + ")";
} else {
label = base + "(Replaced By Fee #2)";
}
} else {
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
}
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
} catch(Exception e) {
log.error("Error creating RBF payment", e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
if(payments.isEmpty()) {
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
return;
}
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
}
private static Double getMaxFeeRate() {
if(AppServices.getTargetBlockFeeRates().isEmpty()) {
return 100.0;
}
return AppServices.getTargetBlockFeeRates().values().iterator().next();
}
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.collect(Collectors.toList());
if(!utxoEntries.contains(hashIndexEntry)) {
utxoEntries = List.of(hashIndexEntry);
}
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos));
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 unfreezeUtxo(HashIndexEntry hashIndexEntry) {
hashIndexEntry.getHashIndex().setStatus(null);
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
}
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
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 copyDate = new MenuItem("Copy Date");
@@ -155,7 +328,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
@@ -181,11 +354,22 @@ 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(HashIndexEntry hashIndexEntry) {
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 -> {
@@ -194,17 +378,46 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
MenuItem sendSelected = new MenuItem("Send Selected");
sendSelected.setOnAction(AE -> {
hide();
sendSelectedUtxos(treeTableView, hashIndexEntry);
});
getItems().add(sendSelected);
}
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.setOnAction(AE -> {
hide();
freezeUtxo(hashIndexEntry);
});
getItems().add(freezeUtxo);
} else {
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
unfreezeUtxo.setOnAction(AE -> {
hide();
unfreezeUtxo(hashIndexEntry);
});
getItems().add(unfreezeUtxo);
}
}
}
}
public static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
if(entry != null) {
if(entry instanceof TransactionEntry) {
@@ -222,6 +435,10 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry) {
cell.getStyleClass().add("utxo-row");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
@@ -11,10 +11,12 @@ import javafx.scene.input.ClipboardContent;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Currency;
import java.util.Locale;
public class FiatLabel extends CopyableLabel {
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00");
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
@@ -2,6 +2,10 @@ package com.sparrowwallet.sparrow.control;
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.Wallet;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport;
import com.sparrowwallet.sparrow.io.ImportException;
import javafx.beans.property.SimpleStringProperty;
@@ -9,41 +13,75 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
public abstract class FileImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
private final FileImport importer;
private Button importButton;
protected ButtonBase importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
private final boolean scannable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl) {
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
super(title, description, content, imageUrl);
this.importer = importer;
this.scannable = scannable;
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importFile();
});
return importButton;
if(scannable) {
ToggleButton scanButton = new ToggleButton("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
scanButton.setGraphic(cameraGlyph);
scanButton.setOnAction(event -> {
scanButton.setSelected(false);
importQR();
});
ToggleButton fileButton = new ToggleButton("Import File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
importFile();
});
importButton = fileButton;
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(scanButton, fileButton);
return segmentedButton;
} else {
importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importFile();
});
return importButton;
}
}
private void importFile() {
@@ -52,8 +90,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json")
new FileChooser.ExtensionFilter("All Files", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json"),
new FileChooser.ExtensionFilter("TXT", "*.txt")
);
File file = fileChooser.showOpenDialog(window);
@@ -93,6 +132,53 @@ public abstract class FileImportPane extends TitledDescriptionPane {
}
}
private void importQR() {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.wallets != null) {
wallets = result.wallets;
try {
importFile(importer.getName(), null, null);
} catch(ImportException e) {
setError("Import Error", e.getMessage());
}
} else if(result.payload != null) {
try {
importFile(importer.getName(), new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)), null);
} catch(Exception e) {
log.error("Error importing QR", e);
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) {
errorMessage = "QR contents were not in JSON format";
}
setError("Import Error", errorMessage);
}
} else if(result.exception != null) {
log.error("Error importing QR", result.exception);
setError("Import Error", result.exception.getMessage());
}
}
}
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
if(wallets != null) {
for(Wallet wallet : wallets) {
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
return wallet.getKeystores().get(0);
}
}
throw new ImportException("Script type " + scriptType + " is not supported");
}
return null;
}
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
private Node getPasswordEntry(File file) {
@@ -102,6 +188,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
HBox.setHgrow(passwordField, Priority.ALWAYS);
Button importEncryptedButton = new Button("Import");
importEncryptedButton.setDefaultButton(true);
importEncryptedButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -14,13 +14,17 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeystoreFileImport importer;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
super(importer, importer.getName(), "Keystore file import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
this.wallet = wallet;
this.importer = importer;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
Keystore keystore = getScannedKeystore(wallet.getScriptType());
if(keystore == null) {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
@@ -2,49 +2,90 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
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 javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
private final boolean scannable;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
Button exportButton = new Button("Export Wallet...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportWallet();
});
return exportButton;
if(scannable) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportWallet() {
private void exportQR() {
exportWallet(null);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
String extension = exporter.getExportFileExtension(wallet);
fileChooser.setInitialFileName(wallet.getName() + "-" +
exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "") +
(extension == null || extension.isEmpty() ? "" : "." + extension));
File file = fileChooser.showSaveDialog(window);
if(file != null) {
@@ -53,43 +94,60 @@ public class FileWalletExportPane extends TitledDescriptionPane {
}
private void exportWallet(File file) {
Wallet copy = wallet.copy();
if(copy.isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
Wallet copy = wallet.copy();
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done"));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
exportWallet(file, decryptedWallet);
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed"));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
setError("Export Error", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet..."));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
exportWallet(file, copy);
exportWallet(file, wallet);
}
}
private void exportWallet(File file, Wallet decryptedWallet) {
private void exportWallet(File file, Wallet exportWallet) {
try {
OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(decryptedWallet, outputStream);
EventManager.get().post(new WalletExportEvent(decryptedWallet));
if(file != null) {
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) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
} finally {
decryptedWallet.clearPrivate();
}
}
}
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
private final WalletImport importer;
public FileWalletImportPane(WalletImport importer) {
super(importer, importer.getName(), "Wallet file import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
this.importer = importer;
}
@@ -0,0 +1,109 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.ByteStreams;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
private final KeystoreFileImport importer;
private String fileName;
private byte[] fileBytes;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
this.importer = importer;
}
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);
}
setContent(getScriptTypeEntry());
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
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));
EventManager.get().post(new WalletImportEvent(wallet));
}
private Node getScriptTypeEntry() {
Label label = new Label("Script Type:");
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
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.");
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importFileButton = new Button("Import");
importFileButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(scriptTypeComboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
if(e.getCause() instanceof JsonParseException) {
errorMessage = "File was not in JSON format";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, scriptTypeComboBox, helpLabel, region, importFileButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -1,10 +1,15 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
public class HelpLabel extends Label {
@@ -14,8 +19,10 @@ public class HelpLabel extends Label {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
setTooltip(tooltip);
tooltip.setShowDuration(Duration.seconds(15));
getStyleClass().add("help-label");
Platform.runLater(() -> setTooltip(tooltip));
}
private static Glyph getHelpGlyph() {
@@ -36,7 +43,7 @@ public class HelpLabel extends Label {
private StringProperty helpText;
public final void setHelpText(String value) {
helpTextProperty().setValue(value);
helpTextProperty().setValue(value.replace("\\n", "\n"));
}
public final String getHelpText() {
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.text.Font;
import com.sparrowwallet.sparrow.AppServices;
public class IdLabel extends CopyableLabel {
public IdLabel() {
@@ -9,6 +9,6 @@ public class IdLabel extends CopyableLabel {
public IdLabel(String text) {
super(text);
setFont(Font.font("Courier"));
setFont(AppServices.getMonospaceFont());
}
}
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
@@ -25,7 +25,8 @@ public class KeystorePassphraseDialog extends Dialog<String> {
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
@@ -0,0 +1,188 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.chart.*;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
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 Tooltip tooltip;
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize() {
setCreateSymbols(false);
setCursor(Cursor.CROSSHAIR);
setVerticalGridLinesVisible(false);
tooltip = new Tooltip();
tooltip.setShowDelay(Duration.ZERO);
tooltip.setHideDelay(Duration.ZERO);
tooltip.setShowDuration(Duration.INDEFINITE);
Platform.runLater(() -> {
Node node = this.lookup(".plot-content");
Tooltip.install(node, tooltip);
});
}
public void update(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
getData().clear();
if(tooltip.isShowing()) {
tooltip.hide();
}
Map<Date, Set<MempoolRateSize>> periodRateSizes = getPeriodRateSizes(mempoolRateSizes);
Map<Date, String> categories = getCategories(periodRateSizes);
CategoryAxis categoryAxis = (CategoryAxis)getXAxis();
categoryAxis.setTickMarkVisible(false);
categoryAxis.setTickLabelGap(10);
categoryAxis.setAutoRanging(false);
categoryAxis.setCategories(FXCollections.observableArrayList(categories.values()));
categoryAxis.invalidateRange(new ArrayList<>(categories.values()));
categoryAxis.setGapStartAndEnd(false);
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());
if(category != null) {
Optional<String> time = categories.entrySet().stream().filter(entry -> entry.getValue().equals(category)).map(entry -> dateFormatter.format(entry.getKey())).findFirst();
time.ifPresent(s -> tooltip.setGraphic(new ChartTooltip(category, s, getData())));
}
});
long previousFeeRate = 0;
for(Long feeRate : SendController.FEE_RATES_RANGE) {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName(feeRate + "+ vB");
long seriesTotalVSize = 0;
for(Date date : periodRateSizes.keySet()) {
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
long totalVSize = 0;
for(MempoolRateSize rateSize : rateSizes) {
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
totalVSize += rateSize.getVSize();
}
}
series.getData().add(new XYChart.Data<>(categories.get(date), totalVSize));
seriesTotalVSize += totalVSize;
}
if(seriesTotalVSize > 0) {
getData().add(series);
}
previousFeeRate = feeRate;
}
if(categories.keySet().iterator().hasNext()) {
String time = categories.values().iterator().next();
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
numberAxis.setTickLabelsVisible(true);
numberAxis.setOpacity(1);
} else {
numberAxis.setTickLabelsVisible(false);
numberAxis.setOpacity(0);
}
}
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
if(mempoolRateSizes.size() == 1) {
return mempoolRateSizes;
}
LocalDateTime period = LocalDateTime.now().minusHours(MAX_PERIOD_HOURS);
return mempoolRateSizes.entrySet().stream().filter(entry -> {
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
return dateTime.isAfter(period);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(u, v) -> { throw new IllegalStateException("Duplicate dates"); },
TreeMap::new));
}
private Map<Date, String> getCategories(Map<Date, Set<MempoolRateSize>> mempoolHistogram) {
Map<Date, String> categories = new LinkedHashMap<>();
String invisible = "" + '\ufeff';
for(Iterator<Date> iter = mempoolHistogram.keySet().iterator(); iter.hasNext(); ) {
Date date = iter.next();
String label = dateFormatter.format(date);
if(!categories.isEmpty() && iter.hasNext()) {
label = invisible;
invisible += '\ufeff';
}
categories.put(date, label);
}
return categories;
}
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);
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");
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
}
label.setGraphic(circle);
getChildren().add(label);
}
}
}
}
}
}
}
@@ -0,0 +1,328 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.security.SignatureException;
import java.util.List;
import java.util.Optional;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final Wallet wallet;
private WalletNode walletNode;
/**
* Verification only constructor
*/
public MessageSignDialog() {
this(null, null);
}
/**
* Sign and verify with user entered address
*
* @param wallet Wallet to sign with
*/
public MessageSignDialog(Wallet wallet) {
this(wallet, null);
}
/**
* Sign and verify with preset address
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
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) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
}
this.wallet = wallet;
this.walletNode = walletNode;
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");
Image image = new Image("image/seed.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
Field addressField = new Field();
addressField.setText("Address:");
address = new TextField();
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
addressField.getInputs().add(address);
if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString());
}
Field messageField = new Field();
messageField.setText("Message:");
message = new TextArea();
message.setWrapText(true);
message.setPrefRowCount(10);
message.setStyle("-fx-pref-height: 180px");
messageField.getInputs().add(message);
Field signatureField = new Field();
signatureField.setText("Signature:");
signature = new TextArea();
signature.getStyleClass().add("id");
signature.setPrefRowCount(2);
signature.setStyle("-fx-pref-height: 60px");
signature.setWrapText(true);
signatureField.getInputs().add(signature);
fieldset.getChildren().addAll(addressField, messageField, signatureField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
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);
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.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
}
}
});
EventManager.get().register(this);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.APPLY.equals(getResult())) {
event.consume();
return;
}
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
private boolean isValidAddress() {
try {
getAddress();
return true;
} catch (InvalidAddressException e) {
return false;
}
}
private void setWalletNodeFromAddress(Wallet wallet, Address address) {
walletNode = wallet.getWalletAddresses().get(address);
}
private void signMessage() {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
if(wallet.containsSeeds()) {
if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
signUnencryptedKeystore(wallet);
}
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
}
}
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
signature.clear();
signature.appendText(signatureText);
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
}
}
private void signUsbKeystore(Wallet usbWallet) {
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
signature.clear();
signature.appendText(optSignature.get());
}
}
private void verifyMessage() {
try {
//Find ECKey from message and signature
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
boolean verified = false;
try {
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText().trim(), signature.getText().trim(), false);
verified = verifyMessage(signedMessageKey);
} catch(SignatureException e) {
//ignore
}
if(!verified) {
try {
ECKey electrumSignedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText(), true);
verified = verifyMessage(electrumSignedMessageKey);
} catch(SignatureException e) {
//ignore
}
}
if(verified) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.setTitle("Verification Succeeded");
alert.setHeaderText("Verification Succeeded");
alert.setContentText("The signature verified against the message.");
alert.showAndWait();
} else {
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
}
} catch(IllegalArgumentException e) {
AppServices.showErrorDialog("Could not verify message", e.getMessage());
} catch(Exception e) {
log.error("Could not verify message", e);
AppServices.showErrorDialog("Could not verify message", e.getMessage());
}
}
private boolean verifyMessage(ECKey signedMessageKey) throws InvalidAddressException, SignatureException {
Address providedAddress = getAddress();
ScriptType scriptType = providedAddress.getScriptType();
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
Storage storage = event.getStorage(wallet);
if(storage == null) {
//Another window, ignore
return;
}
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), 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"));
Wallet decryptedWallet = decryptWalletService.getValue();
signUnencryptedKeystore(decryptedWallet);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
@@ -23,11 +24,13 @@ import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
@@ -47,6 +50,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private Label invalidLabel;
private Button calculateButton;
private Button backButton;
private Button nextButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
@@ -54,7 +58,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Mnemonic import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
@@ -81,7 +85,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Set Words Length");
enterMnemonicButton.setText("Set Words Size");
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(24);
});
@@ -107,7 +111,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
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"};
for(int i = 0; i < accounts.length; i++) {
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 -> {
@@ -188,16 +193,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton = new Button("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
buttonPane.getChildren().add(confirmButton);
AnchorPane.setRightAnchor(confirmButton, 0.0);
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
calculateButton = new Button("Calculate Seed");
calculateButton.setDisable(true);
@@ -216,6 +219,15 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
nextButton = new Button("Confirm Backup...");
nextButton.setOnAction(event -> {
confirmRecord();
});
nextButton.managedProperty().bind(nextButton.visibleProperty());
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
nextButton.setVisible(false);
nextButton.setDefaultButton(true);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
@@ -247,7 +259,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(backButton, calculateButton);
rightBox.getChildren().addAll(backButton, nextButton, confirmButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
@@ -255,25 +267,34 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
vBox.getChildren().add(buttonPane);
}
return vBox;
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
return stackPane;
}
private void generateNew() {
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstanceStrong();
} catch(NoSuchAlgorithmException e) {
secureRandom = new SecureRandom();
}
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
displayMnemonicCode();
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
setDescription("Write down words before re-entering");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
confirmButton.setVisible(true);
nextButton.setVisible(true);
backButton.setVisible(false);
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
@@ -285,6 +306,36 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
wordEntry.getEditor().setEditable(false);
}
StackPane wordsStackPane = (StackPane)getContent();
if(wordsStackPane.getChildren().size() > 1) {
wordsStackPane.getChildren().remove(1);
confirmButton.setVisible(false);
}
}
private void confirmRecord() {
setDescription("Confirm words have been recorded");
showHideLink.setVisible(false);
StackPane wordsPane = (StackPane)getContent();
StackPane confirmPane = new StackPane();
confirmPane.setMaxWidth(350);
confirmPane.setMaxHeight(100);
Region region = new Region();
region.setMinWidth(confirmPane.getMaxWidth());
region.setMinHeight(confirmPane.getMaxHeight());
confirmPane.getStyleClass().add("box-overlay");
Node wrappedRegion = Borders.wrap(region).lineBorder().innerPadding(0).outerPadding(0).buildAll();
Label label = new Label("Have these " + wordEntriesProperty.get().size() + " words been written down?\nIn the next step, you will need to re-enter them.");
confirmPane.getChildren().addAll(wrappedRegion, label);
wordsPane.getChildren().add(confirmPane);
setExpanded(true);
backButton.setVisible(true);
nextButton.setVisible(false);
confirmButton.setVisible(true);
generateButton.setVisible(false);
}
private void confirmBackup() {
@@ -349,6 +400,19 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField.setMaxWidth(100);
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
// if text was added, fix the text to fit the requirements
if(!text.isEmpty()) {
String newText = text.replace(" ", "").toLowerCase();
int carretPos = change.getCaretPosition() - text.length() + newText.length();
change.setText(newText);
// fix caret position based on difference in originally added text and fixed text
change.selectRange(carretPos, carretPos);
}
return change;
});
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
@@ -5,20 +5,22 @@ import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.hummingbird.LegacyUREncoder;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.UREncoder;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
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.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,47 +28,89 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@SuppressWarnings("deprecation")
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 ANIMATION_PERIOD_MILLIS = 400;
private final UR ur;
private final UREncoder encoder;
private final ImageView qrImageView;
private AnimateQRService animateQRService;
private String currentPart;
public QRDisplayDialog(byte[] data) {
this(UR.fromBytes(data));
private boolean useLegacyEncoding;
private String[] legacyParts;
private int legacyPartIndex;
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
this(UR.fromBytes(type, data), addLegacyEncodingOption);
}
public QRDisplayDialog(UR ur) {
this(ur, false);
}
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = getDialogPane();
final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
nextPart();
if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
AnimateQRService animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(100));
animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
animateQRService.start();
setOnCloseRequest(event -> {
animateQRService.cancel();
});
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().add(cancelButtonType);
if(addLegacyEncodingOption) {
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
}
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
public QRDisplayDialog(String data) {
this.ur = null;
this.encoder = null;
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
qrImageView.setImage(getQrCode(data));
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
@@ -76,8 +120,16 @@ public class QRDisplayDialog extends Dialog<UR> {
}
private void nextPart() {
String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase();
if(!useLegacyEncoding) {
String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase();
} else {
currentPart = legacyParts[legacyPartIndex];
legacyPartIndex++;
if(legacyPartIndex > legacyParts.length - 1) {
legacyPartIndex = 0;
}
}
}
private Image getQrCode(String fragment) {
@@ -97,6 +149,44 @@ public class QRDisplayDialog extends Dialog<UR> {
return null;
}
private void setUseLegacyEncoding(boolean useLegacyEncoding) {
if(useLegacyEncoding) {
try {
//Force to be bytes type for legacy encoding
LegacyUREncoder legacyEncoder = new LegacyUREncoder(new UR(RegistryType.BYTES.toString(), ur.getCborBytes()));
this.legacyParts = legacyEncoder.encode();
this.useLegacyEncoding = true;
if(legacyParts.length == 1) {
if(animateQRService != null) {
animateQRService.cancel();
}
nextPart();
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
} catch(UR.InvalidTypeException e) {
//Can't happen
}
} else {
this.useLegacyEncoding = false;
if(encoder.isSinglePart()) {
if(animateQRService != null) {
animateQRService.cancel();
}
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
}
}
private class AnimateQRService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
@@ -111,4 +201,40 @@ public class QRDisplayDialog extends Dialog<UR> {
};
}
}
private class QRDisplayDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton legacy = new ToggleButton(buttonType.getText());
legacy.setGraphicTextGap(5);
setLegacyGraphic(legacy, false);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(legacy, buttonData);
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseLegacyEncoding(newValue);
setLegacyGraphic(legacy, newValue);
});
return legacy;
}
return super.createButton(buttonType);
}
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
}
@@ -1,58 +1,142 @@
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.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.sparrow.ur.ResultType;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.URDecoder;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.LegacyURDecoder;
import com.sparrowwallet.hummingbird.registry.*;
import com.sparrowwallet.hummingbird.ResultType;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.URDecoder;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.layout.StackPane;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
@SuppressWarnings("deprecation")
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final URDecoder decoder;
private final WebcamService webcamService;
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
private final URDecoder decoder;
private final LegacyURDecoder legacyDecoder;
private final WebcamService webcamService;
private List<String> parts;
private boolean isUr;
private QRScanDialog.Result result;
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
private static final int SCAN_PERIOD_MILLIS = 100;
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
this.webcamService = new WebcamService(WebcamResolution.VGA);
if(Config.get().isHdCapture()) {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
final DialogPane dialogPane = getDialogPane();
final DialogPane dialogPane = new QRScanDialogPane();
setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
ProgressBar progressBar = new ProgressBar();
progressBar.setMinHeight(20);
progressBar.setPadding(new Insets(0, 10, 0, 10));
progressBar.setPrefWidth(Integer.MAX_VALUE);
progressBar.progressProperty().bind(percentComplete);
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
});
VBox vBox = new VBox(20);
vBox.getChildren().addAll(wrappedView, progressBar);
dialogPane.setContent(vBox);
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
});
webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
}
webcamService.cancel();
});
setOnCloseRequest(event -> {
Platform.runLater(webcamService::cancel);
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
if(Config.get().isHdCapture() != isHdCapture) {
Config.get().setHdCapture(isHdCapture);
}
Platform.runLater(() -> webcamResolutionProperty.set(null));
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(660);
dialogPane.setPrefHeight(550);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
}
@@ -66,43 +150,87 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//Try text first
String qrtext = qrResult.getText();
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
isUr = true;
decoder.receivePart(qrtext);
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) {
try {
PSBT psbt = new PSBT(urResult.ur.toBytes());
result = new Result(psbt);
return;
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
legacyDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
try {
Transaction transaction = new Transaction(urResult.ur.toBytes());
result = new Result(transaction);
return;
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction");
} else {
result = new Result("Cannot parse UR type of " + urResult.ur.getType());
if(legacyDecoder.isComplete()) {
try {
UR ur = legacyDecoder.decode();
result = extractResultFromUR(ur);
} catch(Exception e) {
result = new Result(new URException(e.getMessage()));
}
} else {
result = new Result(urResult.error);
}
} else {
decoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete()));
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
result = extractResultFromUR(urResult.ur);
} else {
result = new Result(new URException(urResult.error));
}
}
}
} else if(partMatcher.matches()) {
int m = Integer.parseInt(partMatcher.group(1));
int n = Integer.parseInt(partMatcher.group(2));
String payload = partMatcher.group(3);
if(parts == null) {
parts = new ArrayList<>(n);
IntStream.range(0, n).forEach(i -> parts.add(null));
}
parts.set(m - 1, payload);
if(n > 0) {
Platform.runLater(() -> percentComplete.setValue((double)parts.stream().filter(Objects::nonNull).count() / n));
}
if(parts.stream().filter(Objects::nonNull).count() == n) {
String complete = String.join("", parts);
try {
PSBT psbt = PSBT.fromString(complete);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
if(PSBT.isPSBT(complete)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
try {
Transaction transaction = new Transaction(Utils.hexToBytes(complete));
result = new Result(transaction);
return;
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
}
} else {
PSBT psbt;
Transaction transaction;
BitcoinURI bitcoinURI;
Address address;
ExtendedKey extendedKey;
try {
extendedKey = ExtendedKey.fromDescriptor(qrtext);
result = new Result(extendedKey);
return;
} catch(Exception e) {
//Ignore, not a valid xpub
}
try {
bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI);
@@ -123,6 +251,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
psbt = PSBT.fromString(qrtext);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
if(PSBT.isPSBT(qrtext)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//Ignore, not parseable as Base64 or hex
}
@@ -153,7 +285,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//Try Base43 used by Electrum
try {
psbt = new PSBT(Base43.decode(qrResult.getText()));
psbt = new PSBT(Base43.decode(qrtext));
result = new Result(psbt);
return;
} catch(Exception e) {
@@ -161,30 +293,294 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
try {
transaction = new Transaction(Base43.decode(qrResult.getText()));
transaction = new Transaction(Base43.decode(qrtext));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
result = new Result("Cannot parse QR code into a PSBT, transaction or address");
result = new Result(qrtext);
}
}
private Result extractResultFromUR(UR ur) {
try {
RegistryType urRegistryType = ur.getRegistryType();
if(urRegistryType.equals(RegistryType.BYTES)) {
byte[] urBytes = (byte[])ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(urBytes);
return new Result(psbt);
} catch(PSBTParseException e) {
if(PSBT.isPSBT(urBytes)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
try {
Transaction transaction = new Transaction(urBytes);
return new Result(transaction);
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
try {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
ByteBuffer buf = ByteBuffer.wrap(urBytes);
CharBuffer charBuffer = decoder.decode(buf);
return new Result(charBuffer.toString());
} catch(Exception e) {
//ignore, bytes not parsable as utf-8
}
result = new Result(new URException("Parsed UR of type " + urRegistryType + " was not a PSBT, transaction or UTF-8 text"));
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
return new Result(psbt);
} catch(Exception e) {
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
}
} else if(urRegistryType.equals(RegistryType.CRYPTO_ADDRESS)) {
CryptoAddress cryptoAddress = (CryptoAddress)ur.decodeFromRegistry();
Address address = getAddress(cryptoAddress);
if(address != null) {
return new Result(BitcoinURI.fromAddress(address));
} else {
return new Result(new URException("Unknown " + urRegistryType + " type of " + cryptoAddress.getType()));
}
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
return new Result(extendedKey);
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
return new Result(outputDescriptor);
} else if(urRegistryType.equals(RegistryType.CRYPTO_ACCOUNT)) {
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
List<Wallet> wallets = getWallets(cryptoAccount);
return new Result(wallets);
} else {
log.error("Unsupported UR type " + urRegistryType);
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
}
} catch(IllegalArgumentException e) {
log.error("Unknown UR type of " + ur.getType(), e);
return new Result(new URException("Unknown UR type of " + ur.getType(), e));
} catch(UR.InvalidCBORException e) {
log.error("Invalid CBOR in UR", e);
return new Result(new URException("Invalid CBOR in UR", e));
} catch(Exception e) {
log.error("Error parsing UR CBOR", e);
return new Result(new URException("Error parsing UR CBOR", e));
}
return null;
}
private Address getAddress(CryptoAddress cryptoAddress) {
Address address = null;
if(cryptoAddress.getType() == null || cryptoAddress.getType() == CryptoAddress.Type.P2PKH) {
address = new P2PKHAddress(cryptoAddress.getData());
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2SH) {
address = new P2SHAddress(cryptoAddress.getData());
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2WPKH) {
address = new P2WPKHAddress(cryptoAddress.getData());
}
return address;
}
private ExtendedKey getExtendedKey(CryptoHDKey cryptoHDKey) {
if(cryptoHDKey.isPrivateKey()) {
DeterministicKey prvKey = HDKeyDerivation.createMasterPrivKeyFromBytes(Arrays.copyOfRange(cryptoHDKey.getKey(), 1, 33), cryptoHDKey.getChainCode(), List.of(ChildNumber.ZERO));
return new ExtendedKey(prvKey, new byte[4], ChildNumber.ZERO);
} else {
ChildNumber lastChild = ChildNumber.ZERO;
int depth = 1;
byte[] parentFingerprint = new byte[4];
if(cryptoHDKey.getOrigin() != null) {
if(!cryptoHDKey.getOrigin().getComponents().isEmpty()) {
PathComponent lastComponent = cryptoHDKey.getOrigin().getComponents().get(cryptoHDKey.getOrigin().getComponents().size() - 1);
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
depth = cryptoHDKey.getOrigin().getComponents().size();
}
if(cryptoHDKey.getParentFingerprint() != null) {
parentFingerprint = cryptoHDKey.getParentFingerprint();
}
}
DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint);
return new ExtendedKey(pubKey, parentFingerprint, lastChild);
}
}
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
if(cryptoOutput.getMultiKey() != null) {
MultiKey multiKey = cryptoOutput.getMultiKey();
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
for(CryptoHDKey cryptoHDKey : multiKey.getHdKeys()) {
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
KeyDerivation keyDerivation = getKeyDerivation(cryptoHDKey.getOrigin());
extendedPublicKeys.put(extendedKey, keyDerivation);
}
return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys);
} else if(cryptoOutput.getEcKey() != null) {
throw new IllegalArgumentException("EC keys are currently unsupported");
} else if(cryptoOutput.getHdKey() != null) {
ExtendedKey extendedKey = getExtendedKey(cryptoOutput.getHdKey());
KeyDerivation keyDerivation = getKeyDerivation(cryptoOutput.getHdKey().getOrigin());
return new OutputDescriptor(scriptType, extendedKey, keyDerivation);
}
throw new IllegalStateException("CryptoOutput did not contain sufficient information");
}
private List<Wallet> getWallets(CryptoAccount cryptoAccount) {
List<Wallet> wallets = new ArrayList<>();
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
Wallet wallet = new Wallet();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
if(outputDescriptor.isMultisig()) {
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
}
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
wallet.setScriptType(outputDescriptor.getScriptType());
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
wallets.add(wallet);
}
return wallets;
}
private ScriptType getScriptType(List<ScriptExpression> expressions) {
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)) {
return ScriptType.P2SH_P2WPKH;
} else if(List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
return ScriptType.P2WPKH;
} else if(List.of(ScriptExpression.SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2SH;
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2SH_P2WSH;
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2WSH;
}
throw new IllegalArgumentException("Unknown script of " + expressions);
}
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
if(cryptoKeypath != null) {
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
}
return null;
}
}
private class QRScanListener implements WebcamListener {
@Override
public void webcamOpen(WebcamEvent webcamEvent) {
}
@Override
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
}
@Override
public void webcamDisposed(WebcamEvent webcamEvent) {
}
@Override
public void webcamImageObtained(WebcamEvent webcamEvent) {
}
}
private class QRScanDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button = null;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton hd = new ToggleButton(buttonType.getText());
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
hd.setGraphicTextGap(5);
setHdGraphic(hd, hd.isSelected());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(hd, buttonData);
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
setHdGraphic(hd, newValue);
});
button = hd;
} else {
button = super.createButton(buttonType);
}
button.disableProperty().bind(webcamService.openingProperty());
return button;
}
private void setHdGraphic(ToggleButton hd, boolean isHd) {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
public final BitcoinURI uri;
public final String error;
public final ExtendedKey extendedKey;
public final OutputDescriptor outputDescriptor;
public final List<Wallet> wallets;
public final String payload;
public final Throwable exception;
public Result(Transaction transaction) {
this.transaction = transaction;
this.psbt = null;
this.uri = null;
this.error = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
@@ -192,7 +588,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null;
this.psbt = psbt;
this.uri = null;
this.error = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
@@ -200,7 +599,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null;
this.psbt = null;
this.uri = uri;
this.error = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
@@ -208,15 +610,54 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null;
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.error = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(String error) {
public Result(ExtendedKey extendedKey) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = error;
this.extendedKey = extendedKey;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(OutputDescriptor outputDescriptor) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = outputDescriptor;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(List<Wallet> wallets) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = wallets;
this.payload = null;
this.exception = null;
}
public Result(String payload) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = payload;
this.exception = null;
}
@@ -224,8 +665,53 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = exception;
}
}
public static class ScanException extends Exception {
public ScanException() {
super();
}
public ScanException(String message) {
super(message);
}
public ScanException(Throwable cause) {
super(cause);
}
public ScanException(String message, Throwable cause) {
super(message, cause);
}
}
public static class URException extends ScanException {
public URException() {
super();
}
public URException(String message) {
super(message);
}
public URException(Throwable cause) {
super(cause);
}
public URException(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);
}
}
}
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu;
import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration;
@@ -1,4 +1,4 @@
package com.sparrowwallet.sparrow.transaction;
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
@@ -33,7 +33,7 @@ public class ScriptContextMenu extends ContextMenu {
});
getItems().add(copyvalue);
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: sans-serif; -fx-font-size: 1em;");
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
area.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
hoverChunk = null;
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
@@ -10,7 +10,8 @@ import javafx.scene.layout.StackPane;
public class SeedDisplayDialog extends Dialog<Void> {
public SeedDisplayDialog(Keystore decryptedKeystore) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
int height = lines * 40;
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreSignedEvent;
@@ -7,8 +8,8 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
@@ -18,6 +19,9 @@ import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.control.SegmentedBar;
import java.util.ArrayList;
import java.util.List;
public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.SignatureProgressSegment> {
public SignaturesProgressBar() {
setOrientation(Orientation.HORIZONTAL);
@@ -25,10 +29,11 @@ public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.Si
setInfoNodeFactory(segment -> segment.getKeystore() == null ? null : new SignatureProgressSegmentLabel(segment.getKeystore().getLabel()));
}
public void initialize(ObservableList<Keystore> signedKeystores, int threshold) {
public void initialize(ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap, int threshold) {
getStyleClass().add("signatures-progress-bar");
getSegments().clear();
List<Keystore> signedKeystores = new ArrayList<>(signatureKeystoreMap.values());
int numSegments = Math.max(threshold, signedKeystores.size());
double segmentSize = 100d / numSegments;
for(int i = 0; i < numSegments; i++) {
@@ -39,19 +44,20 @@ public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.Si
}
}
signedKeystores.addListener((ListChangeListener<Keystore>) c -> {
int numSegments1 = Math.max(threshold, c.getList().size());
double newSegmentSize = 100d / numSegments1;
signatureKeystoreMap.addListener((MapChangeListener<TransactionSignature, Keystore>) c -> {
List<Keystore> newSignedKeystores = new ArrayList<>(c.getMap().values());
int newNumSegments = Math.max(threshold, newSignedKeystores.size());
double newSegmentSize = 100d / newNumSegments;
for(int i = 0; i < numSegments1; i++) {
for(int i = 0; i < newNumSegments; i++) {
SignatureProgressSegment segment = null;
if(i < getSegments().size()) {
segment = getSegments().get(i);
}
Keystore signedKeystore = null;
if(i < signedKeystores.size()) {
signedKeystore = signedKeystores.get(i);
if(i < newSignedKeystores.size()) {
signedKeystore = newSignedKeystores.get(i);
}
if(segment != null) {
@@ -1,9 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
public class TextAreaDialog extends Dialog<String> {
private final TextArea textArea;
@@ -16,15 +20,22 @@ public class TextAreaDialog extends Dialog<String> {
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
final DialogPane dialogPane = getDialogPane();
Image image = new Image("/image/sparrow-small.png");
dialogPane.setGraphic(new ImageView(image));
HBox hbox = new HBox();
this.textArea = new TextArea(defaultValue);
this.textArea.setMaxWidth(Double.MAX_VALUE);
this.textArea.setWrapText(true);
this.textArea.getStyleClass().add("fixed-width");
hbox.getChildren().add(textArea);
HBox.setHgrow(this.textArea, Priority.ALWAYS);
this.defaultValue = defaultValue;
getDialogPane().setContent(hbox);
dialogPane.setContent(hbox);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
@@ -35,6 +46,9 @@ public class TextAreaDialog extends Dialog<String> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? textArea.getText() : null;
});
dialogPane.setPrefWidth(700);
dialogPane.setPrefHeight(400);
}
public final TextArea getEditor() {
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -21,7 +21,7 @@ public class TitledDescriptionPane extends TitledPane {
protected HBox buttonBox;
public TitledDescriptionPane(String title, String description, String content, String imageUrl) {
getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
getStyleClass().add("titled-description-pane");
setPadding(Insets.EMPTY);
@@ -61,7 +61,7 @@ public class TitledDescriptionPane extends TitledPane {
descriptionLabel = new Label(description);
descriptionLabel.getStyleClass().add("description-label");
showHideLink = new Hyperlink("Show Details...");
showHideLink = new Hyperlink("Details...");
showHideLink.managedProperty().bind(showHideLink.visibleProperty());
showHideLink.setOnAction(event -> {
setExpanded(!this.isExpanded());
@@ -88,7 +88,7 @@ public class TitledDescriptionPane extends TitledPane {
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
//Hack to force listItem to expand to full available width less border
listItem.setPrefWidth(newValue.getWidth() - 2);
listItem.setPrefWidth(newValue.getWidth() - 3);
});
return listItem;
@@ -1,31 +1,46 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNode;
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.glyphfont.FontAwesome5;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.util.Duration;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.util.*;
import java.util.stream.Collectors;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final double DIAGRAM_HEIGHT = 230.0;
private static final int MAX_UTXOS = 7;
private static final int MAX_PAYMENTS = 5;
private static final double DIAGRAM_HEIGHT = 215.0;
private static final int TOOLTIP_SHOW_DELAY = 50;
private WalletTransaction walletTx;
public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight());
if(walletTx == null) {
getChildren().clear();
} else {
@@ -49,10 +64,12 @@ public class TransactionDiagram extends GridPane {
Pane txPane = getTransactionPane();
GridPane.setConstraints(txPane, 3, 0);
Pane outputsLinesPane = getOutputsLines();
List<Payment> displayedPayments = getDisplayedPayments();
Pane outputsLinesPane = getOutputsLines(displayedPayments);
GridPane.setConstraints(outputsLinesPane, 4, 0);
Pane outputsPane = getOutputsLabels();
Pane outputsPane = getOutputsLabels(displayedPayments);
GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear();
@@ -62,11 +79,16 @@ public class TransactionDiagram extends GridPane {
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
if(getPayjoinURI() != null) {
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
}
if(selectedUtxos.size() > MAX_UTXOS) {
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
List<BlockTransactionHashIndex> additional = new ArrayList<>();
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
if (utxos.size() < MAX_UTXOS) {
if(utxos.size() < MAX_UTXOS - 1) {
utxos.put(reference, selectedUtxos.get(reference));
} else {
additional.add(reference);
@@ -80,6 +102,22 @@ public class TransactionDiagram extends GridPane {
}
}
private BitcoinURI getPayjoinURI() {
for(Payment payment : walletTx.getPayments()) {
try {
Address address = payment.getAddress();
BitcoinURI bitcoinURI = AppServices.getPayjoinURI(address);
if(bitcoinURI != null) {
return bitcoinURI;
}
} catch(Exception e) {
//ignore
}
}
return null;
}
private Pane getInputsType(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
StackPane stackPane = new StackPane();
@@ -96,7 +134,7 @@ public class TransactionDiagram extends GridPane {
Line topYaxis = new Line();
topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(DIAGRAM_HEIGHT * 0.5 - 20.0);
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(0);
topYaxis.getStyleClass().add("inputs-type");
@@ -110,16 +148,16 @@ public class TransactionDiagram extends GridPane {
Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(DIAGRAM_HEIGHT);
bottomYaxis.setStartY(getDiagramHeight());
bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(DIAGRAM_HEIGHT * 0.5 + 20.0);
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(DIAGRAM_HEIGHT);
bottomBracket.setStartY(getDiagramHeight());
bottomBracket.setEndX(width);
bottomBracket.setEndY(DIAGRAM_HEIGHT);
bottomBracket.setEndY(getDiagramHeight());
bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
@@ -146,24 +184,39 @@ public class TransactionDiagram extends GridPane {
WalletNode walletNode = displayedUtxos.get(input);
String desc = getInputDescription(input);
Label label = new Label(desc);
label.getStyleClass().add("utxo-label");
Button excludeUtxoButton = new Button("");
excludeUtxoButton.setGraphic(getExcludeGlyph());
excludeUtxoButton.setOnAction(event -> {
EventManager.get().post(new ExcludeUtxoEvent(walletTx, input));
});
Tooltip tooltip = new Tooltip();
if(walletNode != null) {
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath().replace("m", "..") + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
tooltip.getStyleClass().add("input-label");
if(input.getLabel() == null || input.getLabel().isEmpty()) {
label.getStyleClass().add("input-label");
} else {
tooltip.getStyleClass().add("input-label");
}
label.setGraphic(excludeUtxoButton);
label.setContentDisplay(ContentDisplay.LEFT);
} else {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex)input;
StringJoiner joiner = new StringJoiner("\n");
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(getInputDescription(additionalInput));
if(input instanceof PayjoinBlockTransactionHashIndex) {
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
} else {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
StringJoiner joiner = new StringJoiner("\n");
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(getInputDescription(additionalInput));
}
tooltip.setText(joiner.toString());
}
tooltip.setText(joiner.toString());
tooltip.getStyleClass().add("input-label");
}
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
label.setTooltip(tooltip);
inputsBox.getChildren().add(label);
@@ -190,23 +243,28 @@ public class TransactionDiagram extends GridPane {
yaxisLine.setStartX(0);
yaxisLine.setStartY(0);
yaxisLine.setEndX(0);
yaxisLine.setEndY(DIAGRAM_HEIGHT);
yaxisLine.setEndY(getDiagramHeight());
yaxisLine.getStyleClass().add("boundary");
group.getChildren().add(yaxisLine);
double width = 140.0;
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
int numUtxos = displayedUtxos.size();
for(int i = 1; i <= numUtxos; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("input-line");
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
curve.getStyleClass().add("input-dashed-line");
}
curve.setStartX(0);
double scaleFactor = (double)i / (numUtxos + 1);
int nodeHeight = 17;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setStartY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setStartY(scale(getDiagramHeight(), scaleFactor, additional));
curve.setEndX(width);
curve.setEndY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
curve.setControlX1(scale(width, 0.2, 0));
curve.setControlY1(curve.getStartY());
@@ -224,7 +282,28 @@ public class TransactionDiagram extends GridPane {
return value * (1.0 - scaleFactor) + additional;
}
private Pane getOutputsLines() {
private List<Payment> getDisplayedPayments() {
List<Payment> payments = walletTx.getPayments();
if(payments.size() > MAX_PAYMENTS) {
List<Payment> displayedPayments = new ArrayList<>();
List<Payment> additional = new ArrayList<>();
for(Payment payment : payments) {
if(displayedPayments.size() < MAX_PAYMENTS - 1) {
displayedPayments.add(payment);
} else {
additional.add(payment);
}
}
displayedPayments.add(new AdditionalPayment(additional));
return displayedPayments;
} else {
return payments;
}
}
private Pane getOutputsLines(List<Payment> displayedPayments) {
VBox pane = new VBox();
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
@@ -238,18 +317,18 @@ public class TransactionDiagram extends GridPane {
group.getChildren().add(yaxisLine);
double width = 140.0;
int numOutputs = (walletTx.getChangeNode() == null ? 2 : 3);
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
curve.setStartX(0);
curve.setStartY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
curve.setEndX(width);
double scaleFactor = (double)i / (numOutputs + 1);
int nodeHeight = 20;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setEndY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
curve.setControlX1(scale(width, 0.2, 0));
curve.controlY1Property().bind(curve.startYProperty());
@@ -263,26 +342,34 @@ public class TransactionDiagram extends GridPane {
return pane;
}
private Pane getOutputsLabels() {
private Pane getOutputsLabels(List<Payment> displayedPayments) {
VBox outputsBox = new VBox();
outputsBox.setMaxWidth(150);
outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer());
String recipientDesc = walletTx.getRecipientAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, getSendGlyph());
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
Tooltip recipientTooltip = new Tooltip("Send " + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
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());
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()));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
outputsBox.getChildren().add(createSpacer());
}
if(walletTx.getChangeNode() != null) {
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString());
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip);
outputsBox.getChildren().add(changeLabel);
outputsBox.getChildren().add(createSpacer());
@@ -294,6 +381,7 @@ public class TransactionDiagram extends GridPane {
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
feeTooltip.getStyleClass().add("fee-tooltip");
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
feeLabel.setTooltip(feeTooltip);
outputsBox.getChildren().add(feeLabel);
outputsBox.getChildren().add(createSpacer());
@@ -309,7 +397,9 @@ public class TransactionDiagram extends GridPane {
String txDesc = "Transaction";
Label txLabel = new Label(txDesc);
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip);
txPane.getChildren().add(txLabel);
txPane.getChildren().add(createSpacer());
@@ -317,47 +407,91 @@ public class TransactionDiagram extends GridPane {
return txPane;
}
public double getDiagramHeight() {
if(AppServices.isReducedWindowHeight(this)) {
return DIAGRAM_HEIGHT - 40;
}
return DIAGRAM_HEIGHT;
}
private Node createSpacer() {
final Region spacer = new Region();
VBox.setVgrow(spacer, Priority.ALWAYS);
return spacer;
}
private Glyph getSendGlyph() {
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.getStyleClass().add("send-icon");
sendGlyph.setFontSize(12);
return sendGlyph;
public static Glyph getExcludeGlyph() {
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
excludeGlyph.getStyleClass().add("exclude-utxo");
excludeGlyph.setFontSize(12);
return excludeGlyph;
}
private Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS);
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getTxoGlyph() {
return getChangeGlyph();
}
public static Glyph getPayjoinGlyph() {
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
payjoinGlyph.getStyleClass().add("payjoin-icon");
payjoinGlyph.setFontSize(12);
return payjoinGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
private Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.HAND_HOLDING);
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
private Glyph getWarningGlyph() {
Glyph feeWarningGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.LOCK);
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
public PayjoinBlockTransactionHashIndex() {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
}
@Override
public String getLabel() {
return "Payjoin input";
}
}
private static class AdditionalBlockTransactionHashIndex extends BlockTransactionHashIndex {
private final List<BlockTransactionHashIndex> additionalInputs;
@@ -375,4 +509,17 @@ public class TransactionDiagram extends GridPane {
return additionalInputs;
}
}
private static class AdditionalPayment extends Payment {
private final List<Payment> additionalPayments;
public AdditionalPayment(List<Payment> additionalPayments) {
super(null, additionalPayments.size() + " more...", additionalPayments.stream().map(Payment::getAmount).mapToLong(v -> v).sum(), false);
this.additionalPayments = additionalPayments;
}
public String toString() {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
}
}
}
@@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
@@ -23,12 +23,13 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
public TransactionIdDialog() {
this.txid = (CustomTextField) TextFields.createClearableTextField();
txid.setFont(Font.font ("Courier", txid.getFont().getSize()));
txid.setFont(AppServices.getMonospaceFont());
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Load Transaction");
dialogPane.setHeaderText("Enter the transaction ID:");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(550);
dialogPane.setPrefHeight(200);
@@ -5,7 +5,6 @@ import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
@@ -51,7 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
balanceCol.setSortable(true);
getColumns().add(balanceCol);
setPlaceholder(new Label("No transactions"));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
@@ -0,0 +1,102 @@
package com.sparrowwallet.sparrow.control;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BaseMultiResolutionImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TrayManager {
private static final Logger log = LoggerFactory.getLogger(TrayManager.class);
private final SystemTray tray;
private final TrayIcon trayIcon;
private final PopupMenu popupMenu = new PopupMenu();
public TrayManager() {
if(!SystemTray.isSupported()) {
throw new UnsupportedOperationException("SystemTray icons are not supported by the current desktop environment.");
}
tray = SystemTray.getSystemTray();
try {
List<Image> imgList = new ArrayList<>();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
} else {
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@2x.png")));
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@3x.png")));
}
BaseMultiResolutionImage mrImage = new BaseMultiResolutionImage(imgList.toArray(new Image[0]));
this.trayIcon = new TrayIcon(mrImage, "Sparrow", popupMenu);
MenuItem miExit = new MenuItem("Quit Sparrow");
miExit.addActionListener(e -> {
SwingUtilities.invokeLater(() -> { tray.remove(this.trayIcon); });
Platform.exit();
});
this.popupMenu.add(miExit);
} catch(IOException e) {
log.error("Could not load system tray image", e);
throw new IllegalStateException(e);
}
}
public void addStage(Stage stage) {
EventQueue.invokeLater(() -> {
MenuItem miStage = new MenuItem(stage.getTitle());
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
miStage.addActionListener(e -> Platform.runLater(() -> {
stage.show();
EventQueue.invokeLater(() -> {
popupMenu.remove(miStage);
if(popupMenu.getItemCount() == 1) {
Platform.setImplicitExit(true);
SwingUtilities.invokeLater(() -> tray.remove(trayIcon));
}
});
}));
//Make sure it's always at the top
this.popupMenu.insert(miStage,popupMenu.getItemCount() - 1);
if(!isShowing()) {
// Keeps the JVM running even if there are no
// visible JavaFX Stages, otherwise JVM would
// exit and we lose the TrayIcon
Platform.setImplicitExit(false);
SwingUtilities.invokeLater(() -> {
try {
tray.add(this.trayIcon);
} catch(AWTException e) {
log.error("Unable to add system tray icon", e);
}
});
}
});
}
public boolean isShowing() {
return Arrays.stream(tray.getTrayIcons()).collect(Collectors.toList()).contains(trayIcon);
}
public static boolean isSupported() {
return Desktop.isDesktopSupported() && SystemTray.isSupported();
}
}
@@ -39,7 +39,7 @@ public class UtxosChart extends BarChart<String, Number> {
public void update(WalletUtxosEntry walletUtxosEntry) {
List<Data<String, Number>> utxoDataList = walletUtxosEntry.getChildren().stream()
.map(entry -> new XYChart.Data<>(getCategoryName(entry), (Number)entry.getValue(), entry))
.sorted((o1, o2) -> (int) (o2.getYValue().longValue() - o1.getYValue().longValue()))
.sorted((o1, o2) -> Long.compare(o2.getYValue().longValue(), o1.getYValue().longValue()))
.collect(Collectors.toList());
totalUtxos = utxoDataList.size();
@@ -86,11 +86,10 @@ 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);
Node bar = lookup(".data" + i);
if(bar != null) {
if(data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) {
bar.getStyleClass().add("selected");
} else if(data.getExtraValue() == null && entries.size() == totalUtxos) {
if((data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) || (data.getExtraValue() == null && entries.size() == totalUtxos)) {
Node bar = lookup(".data" + i);
if(bar != null) {
bar.getStyleClass().add("selected");
}
}
@@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
@@ -69,7 +68,7 @@ public class UtxosTreeTable extends CoinTreeTable {
getColumns().add(amountCol);
setTreeColumn(amountCol);
setPlaceholder(new Label("No unspent outputs"));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);
@@ -0,0 +1,68 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class WalletBirthDateDialog extends Dialog<Date> {
private final DatePicker birthDatePicker;
public WalletBirthDateDialog(Date birthDate) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
dialogPane.setPrefHeight(200);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HISTORY);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
HBox datePickerBox = new HBox(10);
Label label = new Label("Start scanning from:");
label.setPadding(new Insets(5, 0, 0, 8));
datePickerBox.getChildren().add(label);
birthDatePicker = birthDate == null ? new DatePicker() : new DatePicker(birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
birthDatePicker.setEditable(false);
birthDatePicker.setConverter(new DateStringConverter());
datePickerBox.getChildren().add(birthDatePicker);
dialogPane.setContent(datePickerBox);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
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);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
okButton.disableProperty().bind(isInvalid);
setResultConverter(dialogButton -> dialogButton == okButtonType ? Date.from(birthDatePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) : null);
}
}
@@ -3,13 +3,10 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ColdcardMultisig;
import com.sparrowwallet.sparrow.io.Electrum;
import com.sparrowwallet.sparrow.io.WalletExport;
import com.sparrowwallet.sparrow.io.WalletImport;
import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
@@ -26,6 +23,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
});
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -34,7 +32,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setPrefHeight(400);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
@@ -43,9 +41,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum());
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new Electrum());
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -60,7 +58,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
dialogPane.setPrefHeight(480);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@@ -2,17 +2,26 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.*;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import java.util.List;
public class WalletImportDialog extends Dialog<Wallet> {
private Wallet wallet;
private final Accordion importAccordion;
private final Button scanButton;
public WalletImportDialog() {
EventManager.get().register(this);
@@ -21,6 +30,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
});
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -29,25 +39,40 @@ public class WalletImportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setPrefHeight(420);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletImport> importers = List.of(new ColdcardMultisig(), new Electrum());
Accordion importAccordion = new Accordion();
for(WalletImport importer : importers) {
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
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());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan for Connected Devices", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(scanButtonType, cancelButtonType);
scanButton = (Button) dialogPane.lookupButton(scanButtonType);
scanButton.setGraphic(new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB));
scanButton.addEventFilter(ActionEvent.ACTION, event -> {
scan();
event.consume();
});
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
dialogPane.setPrefHeight(500);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@@ -57,4 +82,26 @@ public class WalletImportDialog extends Dialog<Wallet> {
wallet = event.getWallet();
setResult(wallet);
}
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
scanButton.setGraphic(new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB));
scanButton.setTooltip(null);
List<Device> devices = enumerateService.getValue();
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
for(Device device : devices) {
DevicePane devicePane = new DevicePane(new Wallet(), device);
importAccordion.getPanes().add(0, devicePane);
}
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
glyph.getStyleClass().add("failure");
scanButton.setGraphic(glyph);
scanButton.setTooltip(new Tooltip(workerStateEvent.getSource().getException().getMessage()));
});
enumerateService.start();
}
}
@@ -1,12 +1,16 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
@@ -16,27 +20,74 @@ import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class WalletNameDialog extends Dialog<String> {
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate> {
private final CustomTextField name;
private final CheckBox existingCheck;
private final DatePicker existingPicker;
public WalletNameDialog() {
this.name = (CustomTextField)TextFields.createClearableTextField();
this("");
}
public WalletNameDialog(String initialName) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
setTitle("Wallet Name");
dialogPane.setHeaderText("Enter a name for this wallet:");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
dialogPane.setPrefWidth(460);
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
final VBox content = new VBox(10);
final VBox content = new VBox(20);
name = (CustomTextField)TextFields.createClearableTextField();
name.setText(initialName);
content.getChildren().add(name);
HBox existingBox = new HBox(10);
existingCheck = new CheckBox("Has existing transactions");
existingCheck.setPadding(new Insets(5, 0, 0, 0));
existingBox.getChildren().add(existingCheck);
existingPicker = new DatePicker();
existingPicker.setConverter(new DateStringConverter());
existingPicker.setEditable(false);
existingPicker.setPrefWidth(130);
existingPicker.managedProperty().bind(existingPicker.visibleProperty());
existingPicker.setVisible(false);
existingBox.getChildren().add(existingPicker);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Select an approximate date earlier than the first wallet transaction.");
helpLabel.setTranslateY(5);
helpLabel.managedProperty().bind(helpLabel.visibleProperty());
helpLabel.visibleProperty().bind(existingPicker.visibleProperty());
existingBox.getChildren().add(helpLabel);
existingCheck.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
existingCheck.setText("Has existing transactions starting from");
existingPicker.setVisible(true);
} else {
existingCheck.setText("Has existing transactions");
existingPicker.setVisible(false);
}
});
if(requestBirthDate) {
content.getChildren().add(existingBox);
}
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
@@ -45,18 +96,39 @@ public class WalletNameDialog extends Dialog<String> {
Validator.createEmptyValidator("Wallet name is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue))
));
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("New Wallet", ButtonBar.ButtonData.OK_DONE);
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
name.getText().length() == 0 || Storage.walletExists(name.getText()), name.textProperty());
name.getText().length() == 0 || Storage.walletExists(name.getText()) || (existingCheck.isSelected() && existingPicker.getValue() == null), name.textProperty(), existingCheck.selectedProperty(), existingPicker.valueProperty());
okButton.disableProperty().bind(isInvalid);
name.setPromptText("Wallet Name");
Platform.runLater(name::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? name.getText() : null);
setResultConverter(dialogButton -> dialogButton == okButtonType ? new NameAndBirthDate(name.getText(), existingPicker.getValue()) : null);
}
public static class NameAndBirthDate {
private final String name;
private final Date birthDate;
public NameAndBirthDate(String name, LocalDate birthLocalDate) {
this.name = name;
this.birthDate = (birthLocalDate == null ? null : Date.from(birthLocalDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()));
}
public String getName() {
return name;
}
public Date getBirthDate() {
return birthDate;
}
}
}
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
@@ -21,21 +21,27 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
private final CustomPasswordField password;
private final CustomPasswordField passwordConfirm;
private final CheckBox backupExisting;
public WalletPasswordDialog(PasswordRequirement requirement) {
this(null, requirement);
}
private final CheckBox changePassword;
private final CheckBox deleteBackups;
private boolean addingPassword;
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
this(walletName, requirement, false);
}
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
this.requirement = requirement;
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
this.changePassword = new CheckBox("Change password");
this.deleteBackups = new CheckBox("Delete any backups");
final DialogPane dialogPane = getDialogPane();
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText(walletName != null ? requirement.description.substring(0, requirement.description.length() - 1) + " for " + walletName + ":" : requirement.description);
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(260);
@@ -49,9 +55,21 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
content.getChildren().add(password);
content.getChildren().add(passwordConfirm);
if(requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(changePassword);
changePassword.selectedProperty().addListener((observable, oldValue, newValue) -> {
backupExisting.setVisible(!newValue);
});
changePassword.setSelected(suggestChangePassword);
}
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(backupExisting);
backupExisting.managedProperty().bind(backupExisting.visibleProperty());
deleteBackups.managedProperty().bind(deleteBackups.visibleProperty());
deleteBackups.visibleProperty().bind(backupExisting.visibleProperty().not());
content.getChildren().addAll(backupExisting, deleteBackups);
backupExisting.setSelected(true);
deleteBackups.setSelected(true);
}
dialogPane.setContent(content);
@@ -69,21 +87,25 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
okButton.disableProperty().bind(isInvalid);
if(requirement != PasswordRequirement.UPDATE_NEW) {
if(requirement != PasswordRequirement.UPDATE_NEW && requirement != PasswordRequirement.UPDATE_CHANGE) {
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
}
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY) {
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_CHANGE) {
password.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue.isEmpty()) {
okButton.setText("No Password");
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
backupExisting.setVisible(true);
addingPassword = false;
} else {
okButton.setText("Set Password");
passwordConfirm.setVisible(true);
passwordConfirm.setManaged(true);
backupExisting.setVisible(false);
addingPassword = true;
}
});
}
@@ -96,14 +118,23 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
}
public boolean isBackupExisting() {
return backupExisting.isSelected();
return !(addingPassword || isChangePassword()) && backupExisting.isSelected();
}
public boolean isChangePassword() {
return changePassword.isSelected();
}
public boolean isDeleteBackups() {
return (addingPassword || isChangePassword()) && deleteBackups.isSelected();
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password");
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
private final String description;
private final String okButtonText;
@@ -0,0 +1,354 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.github.sarxos.webcam.ds.buildin.natives.Device;
import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
import org.bridj.Pointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings("deprecation")
public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess, Runnable, WebcamDevice.FPSSource {
private static final Logger LOG = LoggerFactory.getLogger(WebcamScanDevice.class);
private static final int DEVICE_BUFFER_SIZE = 5;
private static final Dimension[] DIMENSIONS;
private static final int[] BAND_OFFSETS;
private static final int[] BITS;
private static final int[] OFFSET;
private static final int DATA_TYPE = 0;
private static final ColorSpace COLOR_SPACE;
public static final int SCAN_LOOP_WAIT_MILLIS = 100;
private int timeout = 5000;
private OpenIMAJGrabber grabber = null;
private Device device = null;
private Dimension size = null;
private ComponentSampleModel smodel = null;
private ColorModel cmodel = null;
private boolean failOnSizeMismatch = false;
private final AtomicBoolean disposed = new AtomicBoolean(false);
private final AtomicBoolean open = new AtomicBoolean(false);
private final AtomicBoolean fresh = new AtomicBoolean(false);
private Thread refresher = null;
private String name = null;
private String id = null;
private String fullname = null;
private long t1 = -1L;
private long t2 = -1L;
private volatile double fps = 0.0D;
protected WebcamScanDevice(Device device) {
this.device = device;
this.name = device.getNameStr();
this.id = device.getIdentifierStr();
this.fullname = String.format("%s %s", this.name, this.id);
}
public String getName() {
return this.fullname;
}
public String getDeviceName() {
return this.name;
}
public String getDeviceId() {
return this.id;
}
public Device getDeviceRef() {
return this.device;
}
public Dimension[] getResolutions() {
return DIMENSIONS;
}
public Dimension getResolution() {
if (this.size == null) {
this.size = this.getResolutions()[0];
}
return this.size;
}
public void setResolution(Dimension size) {
if (size == null) {
throw new IllegalArgumentException("Size cannot be null");
} else if (this.open.get()) {
throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
} else {
this.size = size;
}
}
public ByteBuffer getImageBytes() {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
return null;
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
return null;
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
return null;
} else {
int length = this.size.width * this.size.height * 3;
LOG.trace("Webcam device get buffer, read {} bytes", length);
return image.getByteBuffer((long)length);
}
}
}
public void getImageBytes(ByteBuffer target) {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
} else {
int minSize = this.size.width * this.size.height * 3;
int curSize = target.remaining();
if (minSize > curSize) {
throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
} else {
LOG.trace("Webcam device read buffer {} bytes", minSize);
image = image.validBytes((long)minSize);
image.getBytes(target);
}
}
}
}
public BufferedImage getImage() {
ByteBuffer buffer = this.getImageBytes();
if (buffer == null) {
LOG.error("Images bytes buffer is null!");
return null;
} else {
byte[] bytes = new byte[this.size.width * this.size.height * 3];
byte[][] data = new byte[][]{bytes};
buffer.get(bytes);
DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
WritableRaster raster = Raster.createWritableRaster(this.smodel, dbuf, (Point)null);
BufferedImage bi = new BufferedImage(this.cmodel, raster, false, (Hashtable)null);
bi.flush();
return bi;
}
}
public void open() {
if (!this.disposed.get()) {
LOG.debug("Opening webcam device {}", this.getName());
if (this.size == null) {
this.size = this.getResolutions()[0];
}
if (this.size == null) {
throw new RuntimeException("The resolution size cannot be null");
} else {
LOG.debug("Webcam device {} starting session, size {}", this.device.getIdentifierStr(), this.size);
this.grabber = new OpenIMAJGrabber();
DeviceList list = (DeviceList)this.grabber.getVideoDevices().get();
Iterator var2 = list.asArrayList().iterator();
while(var2.hasNext()) {
Device d = (Device)var2.next();
d.getNameStr();
d.getIdentifierStr();
}
boolean started = this.grabber.startSession(this.size.width, this.size.height, 50, Pointer.pointerTo(this.device));
if (!started) {
throw new WebcamException("Cannot start native grabber!");
} else {
this.grabber.setTimeout(this.timeout);
LOG.debug("Webcam device session started");
Dimension size2 = new Dimension(this.grabber.getWidth(), this.grabber.getHeight());
int w1 = this.size.width;
int w2 = size2.width;
int h1 = this.size.height;
int h2 = size2.height;
if (w1 != w2 || h1 != h2) {
if (this.failOnSizeMismatch) {
throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
}
Object[] args = new Object[]{w1, h1, w2, h2, w2, h2};
LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
this.size = new Dimension(w2, h2);
}
this.smodel = new ComponentSampleModel(0, this.size.width, this.size.height, 3, this.size.width * 3, BAND_OFFSETS);
this.cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, 1, 0);
LOG.debug("Clear memory buffer");
this.clearMemoryBuffer();
LOG.debug("Webcam device {} is now open", this);
this.open.set(true);
this.refresher = this.startFramesRefresher();
}
}
}
}
private void clearMemoryBuffer() {
for(int i = 0; i < 5; ++i) {
this.grabber.nextFrame();
}
}
private Thread startFramesRefresher() {
Thread refresher = new Thread(this, String.format("frames-refresher-[%s]", this.id));
refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
refresher.setDaemon(true);
refresher.start();
return refresher;
}
public void close() {
if (this.open.compareAndSet(true, false)) {
LOG.debug("Closing webcam device");
this.grabber.stopSession();
}
}
public void dispose() {
if (this.disposed.compareAndSet(false, true)) {
LOG.debug("Disposing webcam device {}", this.getName());
this.close();
}
}
public void setFailOnSizeMismatch(boolean fail) {
this.failOnSizeMismatch = fail;
}
public boolean isOpen() {
return this.open.get();
}
public int getTimeout() {
return this.timeout;
}
public void setTimeout(int timeout) {
if (this.isOpen()) {
throw new WebcamException("Timeout must be set before webcam is open");
} else {
this.timeout = timeout;
}
}
private void updateFrameBuffer() {
LOG.trace("Next frame");
if (this.t1 == -1L || this.t2 == -1L) {
this.t1 = System.currentTimeMillis();
this.t2 = System.currentTimeMillis();
}
int result = (new WebcamScanDevice.NextFrameTask(this)).nextFrame();
this.t1 = this.t2;
this.t2 = System.currentTimeMillis();
this.fps = (4.0D * this.fps + (double)(1000L / (this.t2 - this.t1 + 1L))) / 5.0D;
if (result == -1) {
LOG.error("Timeout when requesting image!");
} else if (result < -1) {
LOG.error("Error requesting new frame!");
}
}
public void run() {
do {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
if (Thread.interrupted()) {
LOG.debug("Refresher has been interrupted");
return;
}
if (!this.open.get()) {
LOG.debug("Cancelling refresher");
return;
}
this.updateFrameBuffer();
} while(this.open.get());
}
public double getFPS() {
return this.fps;
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
BITS = new int[]{8, 8, 8};
OFFSET = new int[]{0};
COLOR_SPACE = ColorSpace.getInstance(1000);
}
private class NextFrameTask extends WebcamTask {
private final AtomicInteger result = new AtomicInteger(0);
public NextFrameTask(WebcamDevice device) {
super(device);
}
public int nextFrame() {
try {
this.process();
} catch (InterruptedException var2) {
WebcamScanDevice.LOG.debug("Image buffer request interrupted", var2);
}
return this.result.get();
}
protected void handle() {
WebcamScanDevice device = (WebcamScanDevice)this.getDevice();
if (device.isOpen()) {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
this.result.set(WebcamScanDevice.this.grabber.nextFrame());
WebcamScanDevice.this.fresh.set(true);
}
}
}
}
@@ -0,0 +1,22 @@
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 java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
@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()));
}
return scanDevices;
}
}
@@ -1,27 +1,48 @@
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.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class WebcamService extends Service<Image> {
private final WebcamResolution resolution ;
public class WebcamService extends ScheduledService<Image> {
private WebcamResolution resolution;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
public WebcamService(WebcamResolution resolution) {
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
private Webcam cam;
private long lastQrSampleTime;
static {
Webcam.setDriver(new WebcamScanDriver());
}
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
this.resolution = resolution;
this.listener = listener;
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis();
}
@Override
@@ -29,28 +50,57 @@ public class WebcamService extends Service<Image> {
return new Task<Image>() {
@Override
protected Image call() throws Exception {
Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0);
try {
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
cam.open();
while(!isCancelled()) {
if(cam.isImageNew()) {
BufferedImage bimg = cam.getImage();
updateValue(SwingFXUtils.toFXImage(bimg, null));
readQR(bimg);
if(cam == null) {
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES);
if(webcams.isEmpty()) {
throw new UnsupportedOperationException("No camera available.");
}
cam = webcams.get(0);
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
cam.addWebcamListener(listener);
}
opening.set(true);
cam.open(true, delayCalculator);
opening.set(false);
}
cam.close();
return getValue();
BufferedImage bimg = cam.getImage();
Image image = SwingFXUtils.toFXImage(bimg, null);
updateValue(image);
if(System.currentTimeMillis() > (lastQrSampleTime + QR_SAMPLE_PERIOD_MILLIS)) {
readQR(bimg);
lastQrSampleTime = System.currentTimeMillis();
}
return image;
} finally {
cam.close();
opening.set(false);
}
}
};
}
@Override
public void reset() {
cam = null;
super.reset();
}
@Override
public boolean cancel() {
if(cam != null && !cam.close()) {
cam.close();
}
return super.cancel();
}
private void readQR(BufferedImage bufferedImage) {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
@@ -78,4 +128,16 @@ public class WebcamService extends Service<Image> {
public int getCamHeight() {
return resolution.getSize().height;
}
public void setResolution(WebcamResolution resolution) {
this.resolution = resolution;
}
public boolean isOpening() {
return opening.get();
}
public BooleanProperty openingProperty() {
return opening;
}
}
@@ -1,7 +1,10 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
@@ -14,7 +17,9 @@ public class WebcamView {
private final WebcamService service;
private final Region view;
private final Label statusPlaceholder ;
private final Label statusPlaceholder;
private final ObjectProperty<Image> imageProperty = new SimpleObjectProperty<>(null);
public WebcamView(WebcamService service) {
this.service = service ;
@@ -23,22 +28,32 @@ public class WebcamView {
// make the cam behave like a mirror:
imageView.setScaleX(-1);
service.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
imageProperty.set(newValue);
}
});
this.statusPlaceholder = new Label();
this.view = new Region() {
{
service.stateProperty().addListener((obs, oldState, newState) -> {
switch (newState) {
case READY:
statusPlaceholder.setText("Initializing");
getChildren().setAll(statusPlaceholder);
if(imageProperty.get() == null) {
statusPlaceholder.setText("Initializing");
getChildren().setAll(statusPlaceholder);
}
break ;
case SCHEDULED:
statusPlaceholder.setText("Waiting");
getChildren().setAll(statusPlaceholder);
if(imageProperty.get() == null) {
statusPlaceholder.setText("Waiting");
getChildren().setAll(statusPlaceholder);
}
break ;
case RUNNING:
imageView.imageProperty().unbind();
imageView.imageProperty().bind(service.valueProperty());
imageView.imageProperty().bind(imageProperty);
getChildren().setAll(imageView);
break ;
case CANCELLED:
@@ -1,102 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.Mode;
import javafx.application.HostServices;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.HyperlinkLabel;
import org.controlsfx.control.StatusBar;
import org.controlsfx.control.ToggleSwitch;
public class WelcomeDialog extends Dialog<Mode> {
private static final String[] ELECTRUM_SERVERS = new String[]{
"ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx",
"electrs", "https://github.com/romanz/electrs",
"esplora-electrs", "https://github.com/Blockstream/electrs"};
private final HostServices hostServices;
public WelcomeDialog(HostServices services) {
this.hostServices = services;
final DialogPane dialogPane = getDialogPane();
setTitle("Welcome to Sparrow");
dialogPane.setHeaderText("Welcome to Sparrow!");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(480);
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.OK_DONE);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType("Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(onlineButtonType, offlineButtonType);
final VBox content = new VBox(20);
content.setPadding(new Insets(20, 20, 20, 20));
content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator."));
content.getChildren().add(createParagraph("For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy. Examples include:"));
VBox linkBox = new VBox();
for(int i = 0; i < ELECTRUM_SERVERS.length; i+=2) {
linkBox.getChildren().add(createBulletedLink(ELECTRUM_SERVERS[i], ELECTRUM_SERVERS[i+1]));
}
content.getChildren().add(linkBox);
content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar:"));
content.getChildren().add(createStatusBar(onlineButtonType, offlineButtonType));
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
}
private Label createParagraph(String text) {
Label label = new Label(text);
label.setWrapText(true);
return label;
}
private HyperlinkLabel createBulletedLink(String name, String url) {
HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + name + "]");
label.setOnAction(event -> {
hostServices.showDocument(url);
});
return label;
}
private StatusBar createStatusBar(ButtonType onlineButtonType, ButtonType offlineButtonType) {
StatusBar statusBar = new StatusBar();
statusBar.setText("Online Mode");
statusBar.getRightItems().add(createToggle(statusBar, onlineButtonType, offlineButtonType));
return statusBar;
}
private ToggleSwitch createToggle(StatusBar statusBar, ButtonType onlineButtonType, ButtonType offlineButtonType) {
ToggleSwitch toggleSwitch = new UnlabeledToggleSwitch();
toggleSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> {
Button onlineButton = (Button) getDialogPane().lookupButton(onlineButtonType);
onlineButton.setDefaultButton(newValue);
Button offlineButton = (Button) getDialogPane().lookupButton(offlineButtonType);
offlineButton.setDefaultButton(!newValue);
statusBar.setText(newValue ? "Online Mode" : "Offline Mode");
});
toggleSwitch.setSelected(true);
return toggleSwitch;
}
}
@@ -1,23 +1,18 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import java.util.Map;
public class BlockTransactionFetchedEvent extends PagedEvent {
public class BlockTransactionFetchedEvent extends TransactionReferencesFinishedEvent {
private final Sha256Hash txId;
private final BlockTransaction blockTransaction;
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
this(txId, blockTransaction, inputTransactions, 0, blockTransaction.getTransaction().getInputs().size());
}
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions, int pageStart, int pageEnd) {
super(pageStart, pageEnd);
this.txId = txId;
this.blockTransaction = blockTransaction;
public BlockTransactionFetchedEvent(Transaction transaction, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions, int pageStart, int pageEnd) {
super(transaction, blockTransaction, pageStart, pageEnd);
this.txId = transaction.getTxId();
this.inputTransactions = inputTransactions;
}
@@ -25,10 +20,6 @@ public class BlockTransactionFetchedEvent extends PagedEvent {
return txId;
}
public BlockTransaction getBlockTransaction() {
return blockTransaction;
}
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
return inputTransactions;
}
@@ -1,17 +1,18 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import java.util.List;
public class BlockTransactionOutputsFetchedEvent extends PagedEvent {
public class BlockTransactionOutputsFetchedEvent extends TransactionReferencesEvent {
private final Sha256Hash txId;
private final List<BlockTransaction> outputTransactions;
public BlockTransactionOutputsFetchedEvent(Sha256Hash txId, List<BlockTransaction> outputTransactions, int pageStart, int pageEnd) {
super(pageStart, pageEnd);
this.txId = txId;
public BlockTransactionOutputsFetchedEvent(Transaction transaction, List<BlockTransaction> outputTransactions, int pageStart, int pageEnd) {
super(transaction, pageStart, pageEnd);
this.txId = transaction.getTxId();
this.outputTransactions = outputTransactions;
}
@@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class BwtBootStatusEvent extends BwtStatusEvent {
public BwtBootStatusEvent(String status) {
super(status);
}
}
@@ -0,0 +1,14 @@
package com.sparrowwallet.sparrow.event;
public class BwtElectrumReadyStatusEvent extends BwtStatusEvent {
private final String electrumAddr;
public BwtElectrumReadyStatusEvent(String status, String electrumAddr) {
super(status);
this.electrumAddr = electrumAddr;
}
public String getElectrumAddr() {
return electrumAddr;
}
}
@@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class BwtReadyStatusEvent extends BwtStatusEvent {
public BwtReadyStatusEvent(String status) {
super(status);
}
}
@@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.event;
import java.time.Duration;
public class BwtScanStatusEvent extends BwtStatusEvent {
private final int progress;
private final Duration remainingDuration;
public BwtScanStatusEvent(String status, int progress, Duration remainingDuration) {
super(status);
this.progress = progress;
this.remainingDuration = remainingDuration;
}
public int getProgress() {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public Duration getRemaining() {
return remainingDuration;
}
public String getRemainingAsString() {
if(remainingDuration != null) {
if(progress < 30) {
return Math.round((double)remainingDuration.toSeconds() / 60) + "m";
} else {
return remainingDuration.toMinutesPart() + "m " + remainingDuration.toSecondsPart() + "s";
}
}
return "";
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
/**
* Empty class used to notify the bwt has shut down.
* Note this extends from DisconnectionEvent, which is the more general event fired on any type of disconnection.
*/
public class BwtShutdownEvent extends DisconnectionEvent {
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class BwtStatusEvent {
private final String status;
public BwtStatusEvent(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,34 @@
package com.sparrowwallet.sparrow.event;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class BwtSyncStatusEvent extends BwtStatusEvent {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm");
private final int progress;
private final Date tip;
public BwtSyncStatusEvent(String status, int progress, Date tip) {
super(status);
this.progress = progress;
this.tip = tip;
}
public int getProgress() {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public Date getTip() {
return tip;
}
public String getTipAsString() {
return tip == null ? "" : DATE_FORMAT.format(tip);
}
}
@@ -1,22 +1,26 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final List<String> serverVersion;
private final String serverBanner;
private final int blockHeight;
private final BlockHeader blockHeader;
private final Double minimumRelayFeeRate;
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates) {
super(targetBlockFeeRates);
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
super(targetBlockFeeRates, mempoolRateSizes);
this.serverVersion = serverVersion;
this.serverBanner = serverBanner;
this.blockHeight = blockHeight;
this.blockHeader = blockHeader;
this.minimumRelayFeeRate = minimumRelayFeeRate;
}
public List<String> getServerVersion() {
@@ -34,4 +38,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
public BlockHeader getBlockHeader() {
return blockHeader;
}
public Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate;
}
}
@@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.net.TlsServerException;
public class ConnectionFailedEvent {
private final Throwable exception;
@@ -10,4 +12,26 @@ public class ConnectionFailedEvent {
public Throwable getException() {
return exception;
}
public String getMessage() {
if(exception instanceof TlsServerException) {
return exception.getMessage();
}
Throwable cause = (exception.getCause() != null ? exception.getCause() : exception);
cause = (cause.getCause() != null ? cause.getCause() : cause);
String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error"));
return message + " (" + cause.getMessage() + ")";
}
static String splitCamelCase(String s) {
return s.replaceAll(
String.format("%s|%s|%s",
"(?<=[A-Z])(?=[A-Z][a-z])",
"(?<=[^A-Z])(?=[A-Z])",
"(?<=[A-Za-z])(?=[^A-Za-z])"
),
" "
);
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.AppServices;
public class ConnectionStartEvent {
private final String status;
public ConnectionStartEvent(String serverAddress) {
this.status = AppServices.isTorRunning() ? "Tor running, connecting to " + serverAddress + "..." : "Connecting to " + serverAddress + "...";
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,8 @@
package com.sparrowwallet.sparrow.event;
/**
* Empty class used to signal that the server has been disconnected from.
*/
public class DisconnectionEvent {
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
public class ExcludeUtxoEvent {
private final WalletTransaction walletTransaction;
private final BlockTransactionHashIndex utxo;
public ExcludeUtxoEvent(WalletTransaction walletTransaction, BlockTransactionHashIndex utxo) {
this.walletTransaction = walletTransaction;
this.utxo = utxo;
}
public WalletTransaction getWalletTransaction() {
return walletTransaction;
}
public BlockTransactionHashIndex getUtxo() {
return utxo;
}
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
public class FeeRatesSelectionChangedEvent {
private final Wallet wallet;
private final FeeRatesSelection feeRatesSelection;
public FeeRatesSelectionChangedEvent(Wallet wallet, FeeRatesSelection feeRatesSelection) {
this.wallet = wallet;
this.feeRatesSelection = feeRatesSelection;
}
public Wallet getWallet() {
return wallet;
}
public FeeRatesSelection getFeeRateSelection() {
return feeRatesSelection;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
public class FeeRatesSourceChangedEvent {
private final FeeRatesSource feeRatesSource;
public FeeRatesSourceChangedEvent(FeeRatesSource feeRatesSource) {
this.feeRatesSource = feeRatesSource;
}
public FeeRatesSource getFeeRateSource() {
return feeRatesSource;
}
}
@@ -1,15 +1,24 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.Map;
import java.util.Set;
public class FeeRatesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates;
private final Set<MempoolRateSize> mempoolRateSizes;
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates) {
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
this.targetBlockFeeRates = targetBlockFeeRates;
this.mempoolRateSizes = mempoolRateSizes;
}
public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
public Set<MempoolRateSize> getMempoolRateSizes() {
return mempoolRateSizes;
}
}
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.io.ExchangeSource;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import java.util.Currency;

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