Compare commits

..

585 Commits

Author SHA1 Message Date
Craig Raw
3dc50682a3 upgrade lanterna to v3.1.5 2026-06-23 09:32:44 +02:00
Craig Raw
d5b119e338 bump to v2.5.3 2026-05-31 14:24:55 +02:00
Craig Raw
b457caa5d2 revise wording for non-default sighash warnings 2026-05-31 12:27:16 +02:00
Craig Raw
61ed816c87 improve verification of psbt sighash types 2026-05-31 11:55:38 +02:00
Craig Raw
9a603d7547 make date axis formatter tests locale-stable 2026-05-30 17:57:05 +02:00
PeterXMR
ac666545be
show full year on balance chart x-axis 2026-05-30 16:54:27 +02:00
Craig Raw
c4c77d84e0 use configured unit format in send to many dialog instead of jvm default 2026-05-30 14:04:09 +02:00
Craig Raw
8d126869a6 fix potential off by 1 sat rounding error on imported send to many dialog amounts 2026-05-30 12:32:05 +02:00
Craig Raw
464fade68f improve url validation for auth47 and lnurl-auth 2026-05-30 11:34:53 +02:00
Craig Raw
79517da131 fix potential npes resulting from get transactions 2026-05-28 14:51:24 +02:00
Craig Raw
086297436e followup for precomputed sp outputs 2026-05-26 15:40:31 +02:00
Craig Raw
d69e274b18 warn on loading transactions with non-zero outputs of unknown script type 2026-05-26 12:37:27 +02:00
Craig Raw
d1e67ad4a0 update lark for hid4java 2026-05-24 19:01:46 +02:00
Craig Raw
37ca98c2b0 implement dust detection for sp wallets on received utxos at a higher default limit 2026-05-24 11:23:33 +02:00
Craig Raw
287c943b44 add custom context menu to signature text area in message sign dialog 2026-05-23 15:17:08 +02:00
Craig Raw
cb92f76546 improve loaded psbt verification 2026-05-23 14:52:35 +02:00
Craig Raw
24e9c39cb8 bump to v2.5.2 2026-05-22 18:35:16 +02:00
Craig Raw
754ebf7bbf fix incorrect script type selected in settings on p2tr wallet load 2026-05-22 17:32:41 +02:00
Craig Raw
bc7a0be87e update bip322 implementation to match completed spec 2026-05-22 15:00:53 +02:00
Craig Raw
87af1ed9f5 fix potential npe on transaction entry tooltip 2026-05-22 10:28:17 +02:00
Craig Raw
da476c9d77 bump to v2.5.1 2026-05-21 13:38:15 +02:00
Craig Raw
d8e4582b3c minor ui tweaks followup 2026-05-21 11:45:57 +02:00
Craig Raw
ad5c695946 minor ui tweaks 2026-05-21 11:20:11 +02:00
Craig Raw
3150a96aab bump to v2.5.0 2026-05-21 09:27:26 +02:00
Craig Raw
28521cbc1a update wallet settings help labels 2026-05-20 19:49:49 +02:00
Craig Raw
79bbe7df9f finalize external inputs in cross-wallet psbts to avoid empty witnesses 2026-05-19 18:16:08 +02:00
Craig Raw
e339c9f51a extend post-broadcast mempool poll timeout to support bitcoin core privatebroadcast 2026-05-19 12:27:41 +02:00
Craig Raw
6dd1bda0cb update jzbar to v0.4.0 2026-05-19 11:28:00 +02:00
Craig Raw
9fe0d17aac add frigate.2140.dev public electrum server and auto-select based on requirements for open wallets 2026-05-19 11:04:04 +02:00
Craig Raw
a2eb937fce add bip322 message signing for silent payments wallets 2026-05-18 14:56:50 +02:00
Craig Raw
e985a03a58 persist silent payment address mappings for safe rbf of sp-sending transactions 2026-05-18 12:41:56 +02:00
Craig Raw
273d2aacf3 release electrum transport read lock during socket reads to avoid client request starvation 2026-05-15 11:00:06 +02:00
Craig Raw
97383a4e35 restrict to required sighash types when sending sp outputs 2026-05-15 09:35:29 +02:00
Craig Raw
712750a25c hide receive actions for address entry cells in sp wallets 2026-05-15 08:15:44 +02:00
Craig Raw
1be9ac1072 hide wallet rescan hyperlink when nothing further can be scanned 2026-05-14 12:40:02 +02:00
Craig Raw
a035767e38 default sp wallet birthdate to creation time to avoid full rescans 2026-05-14 11:23:12 +02:00
Craig Raw
1ad237c623 switch electrum server notification detection to streaming json token parse 2026-05-14 10:10:41 +02:00
Craig Raw
7e4aaacd2f preserve sp scan cache cancellation across widening rollback 2026-05-13 13:58:50 +02:00
Craig Raw
60d4cce15b pre-populate sp self-spend node outputs to avoid partial first notification 2026-05-13 11:29:30 +02:00
Craig Raw
7afb27b3e5 increase read timeouts when tor is configured 2026-05-12 16:29:00 +02:00
Craig Raw
8f56e95048 remove bisq broadcast source 2026-05-12 16:19:18 +02:00
Craig Raw
1a5f97cecf discard stale electrum responses with mismatched ids 2026-05-12 15:32:05 +02:00
Craig Raw
db3ebdb300 update bisq broadcast source urls 2026-05-12 15:18:35 +02:00
Craig Raw
0b012995a0 improve wallet update behaviour for silent payment self-sends 2026-05-12 15:17:51 +02:00
Craig Raw
9b12361724 prevent tweakless address nodes in sp wallets 2026-05-08 09:27:08 +02:00
Craig Raw
201d4b8376 refactor transaction diagram to dispatch on output wrapper types 2026-05-07 09:38:15 +02:00
Craig Raw
ab6416f30a fix sp self-spend output discovery by always scanning all batch txs 2026-05-06 15:05:52 +02:00
Craig Raw
e64069f02f implement sp wallet loading 2026-05-04 14:02:52 +02:00
Craig Raw
e96a7113b0 refactor transaction history fetch method 2026-04-30 12:28:46 +02:00
Craig Raw
abe1f9c9e5 add silent payments rpc methods, capability check and notification dispatch 2026-04-29 13:28:44 +02:00
Craig Raw
69aef9c228 add all singlesig importers to sp airgapped keystore import 2026-04-28 09:35:56 +02:00
Craig Raw
630ce0f3ed update bitview logo 2026-04-27 14:59:04 +02:00
Craig Raw
723b004e5f support sp wallet import via all keystore importers 2026-04-27 12:40:33 +02:00
Craig Raw
6cefec5cec avoid unnecessary xpub string roundtrip 2026-04-27 08:28:09 +02:00
Craig Raw
f5ee7bf277 support retrieving silent payments spscan keys via connected devices 2026-04-27 08:13:35 +02:00
Craig Raw
0459f4ca45 add policy type to all keystore import interfaces and factory methods for explicit keystore xpub or spscan field population 2026-04-26 10:17:09 +02:00
Craig Raw
c23dbeedcf improve sp related output descriptor and psbt behaviour 2026-04-24 12:37:07 +02:00
Craig Raw
0bb5e9319f hide gap limit field for sp wallets 2026-04-23 13:53:44 +02:00
Craig Raw
1c5602aa9d implement silent payments receive ui 2026-04-23 13:46:40 +02:00
Craig Raw
c67008840d implement silent payments change outputs and other sp related fixes 2026-04-23 12:23:40 +02:00
Craig Raw
17a04510fd add initial support for sp wallets 2026-04-22 14:57:30 +02:00
Craig Raw
dd50f6973b implement persistence for sp wallets and related drongo changes 2026-04-21 11:49:16 +02:00
Craig Raw
8780e515d5 initial policy type related changes from drongo 2026-04-21 11:34:14 +02:00
Craig Raw
110f887952 add bitview.space as fee rates source 2026-04-09 11:32:20 +02:00
Craig Raw
1143eaa55f only allow sending to payment codes where a notification transaction has previously been sent, even when already linked 2026-04-08 13:51:54 +02:00
Craig Raw
3068ba3988 improve appearance of app notifications after controlsfx upgrade 2026-04-08 12:38:01 +02:00
Craig Raw
58ca52e4c7 upgrade javafx to v26 with headless plaform 2026-04-07 12:36:49 +02:00
Craig Raw
883b7cf3b2 fix concurrent modification of descriptor maps in bitcoind client 2026-04-06 09:12:13 +02:00
Craig Raw
ee5b502a00 improve handling of connected non-jade esp32 devices 2026-03-26 12:14:22 +02:00
Craig Raw
4d7baa070e fix regression to restore save pdf button on descriptor qr display dialog 2026-03-17 11:11:20 +02:00
Craig Raw
fb48643466 load jzbar native dependencies explicitly 2026-03-16 12:00:10 +02:00
Craig Raw
e2c38c2ac4 upgrade jzbar to v0.3.1 to fix library load from application image 2026-03-16 11:33:00 +02:00
Craig Raw
e55dd4401b upgrade usb4java to v1.3.6 to fix library load from application image 2026-03-13 10:42:56 +02:00
Craig Raw
53b6e2532f temporarily revert jzbar application image loading 2026-03-13 09:09:13 +02:00
Craig Raw
98ae891898 load native libraries directly from application image 2026-03-13 07:59:39 +02:00
Craig Raw
019b11c95e upgrade usb4java to allow loading from libraryname system property 2026-03-12 15:38:44 +02:00
Craig Raw
186eacc245 validate bip129 header on import, and fix importing unencrypted bsms files with open wallet 2026-03-12 12:33:56 +02:00
Craig Raw
32a35ed2c0 add bip32 derivation fallback when retreiving signing nodes for high-index inputs 2026-03-11 17:31:26 +02:00
Craig Raw
21f9f9fe25 avoid npe when the extracting signature from a bip322 psbt 2026-03-10 12:07:59 +02:00
Craig Raw
6c6664f29a use psbtv0 for bip322 psbt qr and file exports 2026-03-10 11:35:28 +02:00
Craig Raw
ea0509ca3d bump to v2.4.3 2026-03-10 11:26:47 +02:00
Craig Raw
67e1573329 improve reset instructions for trezor passphrase change 2026-03-09 13:44:32 +02:00
Craig Raw
23a9db5fc6 support loading v3 transactions in the transaction editor 2026-03-06 14:35:42 +02:00
Craig Raw
e9108b85cd fix possible db corruption on password removal by reordering database file and row updates 2026-03-06 11:30:41 +02:00
Craig Raw
3766585474 fix psbtv2 and dst related transaction editor issues around tx version and locktime 2026-03-06 09:15:57 +02:00
Craig Raw
0e7aa34d07 fix non default split menu button text color on dark theme 2026-03-05 13:46:32 +02:00
Craig Raw
537c2ffef0 support qr and file methods for signing messages via psbt when bip322 is selected 2026-03-05 13:37:40 +02:00
Craig Raw
8b4f388eb7 add tests for descriptor import and export, and handle multiline descriptor 2026-03-05 09:39:33 +02:00
Craig Raw
6a07adfea5 upgrade jackson-databind to v2.21.1 2026-03-04 15:14:59 +02:00
Craig Raw
eeade6a173 upgrade gson to v2.13.2 2026-03-04 14:36:29 +02:00
Craig Raw
4c2b8055ec upgrade protobuf to v4.34.0 2026-03-04 14:15:28 +02:00
Craig Raw
04d210ed4b upgrade bcprov to v1.82 and pgpainless to v1.7.7 2026-03-04 14:07:01 +02:00
Craig Raw
5b737be76d upgrade controlsfx to v11.2.3 2026-03-04 13:50:06 +02:00
Craig Raw
908fb80d2a upgrade kotlinx-coroutines-javafx to v1.10.2 2026-03-04 13:37:53 +02:00
Craig Raw
eda7087bdd upgrade junit to v5.14.1 2026-03-04 13:05:19 +02:00
Craig Raw
a5d61f1a9f upgrade jetbrains annotations to v26.0.2 2026-03-04 13:02:21 +02:00
Craig Raw
2621bb6504 upgrade thumbnailator to v0.4.21 2026-03-04 12:59:33 +02:00
Craig Raw
7dfc432bd2 remove streamsupport dependency 2026-03-04 12:54:16 +02:00
Craig Raw
df5b5146ba upgrade jcommander to v3.0 2026-03-04 12:46:10 +02:00
Craig Raw
452584d87d upgrade zxing to v3.5.4 2026-03-04 12:37:24 +02:00
Craig Raw
5c823533d3 upgrade caffeine to v3.2.3 2026-03-04 12:34:52 +02:00
Craig Raw
6561971d4f upgrade dnsjava to v3.6.4 2026-03-04 12:32:05 +02:00
Craig Raw
8ebf8dc041 upgrade rxjava2 to v2.2.21 2026-03-04 12:21:29 +02:00
Craig Raw
419e8e1374 upgrade argon2-jvm to v2.12 2026-03-04 12:14:01 +02:00
Craig Raw
684965cf89 upgrade commons-codec to v1.21.0 2026-03-04 12:09:43 +02:00
Craig Raw
3498b5bab1 upgrade commons-compress to v1.28.0 2026-03-04 12:06:46 +02:00
Craig Raw
74635b1d1f upgrade commons-lang3 to v3.20.0 2026-03-04 12:02:25 +02:00
Craig Raw
229c6430ed upgrade jdbi to v3.51.0 2026-03-04 12:00:14 +02:00
Craig Raw
c74abfeeb7 upgrade hikaricp to v7.0.2 2026-03-04 11:55:59 +02:00
Craig Raw
c78bc7b1cf upgrade openpdf to v1.3.43 2026-03-04 11:49:54 +02:00
Craig Raw
87cd96a627 upgrade jna to v5.18.1 2026-03-04 11:36:08 +02:00
Craig Raw
7ae04a2564 upgrade slf4j-api and jul-to-slf4j to v2.0.17 2026-03-04 11:30:59 +02:00
Craig Raw
d4c0df0df9 detect algorithm on provided certificate when checking for ca cert 2026-02-28 12:51:50 +02:00
Craig Raw
dc88dc4ede add hostname verification for ca-validated tls connections 2026-02-28 12:38:58 +02:00
Craig Raw
a478edfad7 use ca validation or tofu pinning for public servers depending on certificate type 2026-02-27 13:30:46 +02:00
Craig Raw
46d444615c implement tofu certificate pinning for tls bitcoin core connections 2026-02-19 15:35:03 +02:00
Craig Raw
78fe55787b escape backticks in schema name for drop schema statement 2026-02-18 16:16:37 +02:00
Craig Raw
cd0be365de bind cormorant server to localhost only 2026-02-18 15:45:51 +02:00
Craig Raw
b9d62c0b7b bump to v2.4.2 2026-02-17 10:41:24 +02:00
Craig Raw
53d1f196e4 suggest configuring a custom wallets directory when opening a wallet from a non-default location 2026-02-17 09:07:09 +02:00
Craig Raw
64b8da14a6 recognise renamed linux packages for file verify drag and drop 2026-02-16 12:57:28 +02:00
ottosch
529cc3d6e7
handle import of samourai wallet backup file with extraneous appended data 2026-02-12 09:20:40 +02:00
Craig Raw
e13fe897e1 add keepkey passphrase support 2026-02-12 09:17:02 +02:00
Craig Raw
dac1a61601 check wallet file names on open 2026-02-12 09:12:13 +02:00
Craig Raw
227c7ae637 avoid triggering repeated layout passes in address chunking skins 2026-02-11 14:34:21 +02:00
Craig Raw
e11675b9a8 bump to v2.4.1 2026-02-10 09:59:35 +02:00
Craig Raw
146e6cbdc4 improve context menu item text fill on dark theme for default split menu buttons 2026-02-09 13:56:36 +02:00
Craig Raw
e4fa0c9356 bump to v2.4.0 2026-02-09 12:42:16 +02:00
Craig Raw
30719c5a7f final address chunk contrast tweaking 2026-02-09 12:04:32 +02:00
Craig Raw
a120d08ea1 remove extension-less file associations which are no longer supported by jpackage 2026-02-06 17:22:13 +02:00
Craig Raw
b51bd90261 add workaround for jpackage issue where mime-info is not generated on linux 2026-02-06 16:28:33 +02:00
Craig Raw
a765105d34 reduce contrast on address chunks 2026-02-06 13:34:06 +02:00
Craig Raw
2cd7d0bdf3 respect configured bitcoin unit on transaction diagram and label 2026-02-06 12:07:02 +02:00
Craig Raw
c6c24028d9 followup 2026-02-06 11:18:18 +02:00
Craig Raw
efb81555bd add view menu item to toggle address chunk display formatting 2026-02-05 15:38:41 +02:00
Craig Raw
b11afdf2bb format display of addresses in tables only when the table is selected 2026-02-05 12:48:23 +02:00
Craig Raw
779a55c494 update readme to reflect java 25 requirement 2026-02-05 12:34:51 +02:00
Craig Raw
b06d858794 followup 2026-02-05 12:01:53 +02:00
Craig Raw
306c025cbf clear tool cache, revert to setup-java action for reproducibility 2026-02-05 11:59:09 +02:00
Craig Raw
b145bddf07 combine sdkman and setup-java actions 2026-02-05 11:22:24 +02:00
Craig Raw
edee72d97d followup 2 2026-02-05 11:05:27 +02:00
Craig Raw
b87650a740 followup 2026-02-05 10:58:42 +02:00
Craig Raw
0c9bbc2550 define sdkmanrc and reference it in github action 2026-02-05 10:55:08 +02:00
Craig Raw
088f8e302c upgrade kmp-tor to fix file deletion issue on windows 2026-02-04 16:59:42 +02:00
Craig Raw
4309216ad7 fix file association exception on windows 2026-02-04 14:31:05 +02:00
Craig Raw
dffaf806cd remove warnings on gradle test task 2026-02-04 12:57:19 +02:00
Craig Raw
448067db54 upgrade to java 25.0.2 and javafx 25.0.2 2026-02-04 12:18:25 +02:00
Craig Raw
6b498c28c2 format display of addresses in 4 character chunks 2026-02-03 15:11:29 +02:00
Craig Raw
4724dc7700 further minor qr display dialog updates 2026-01-28 15:12:18 +02:00
Craig Raw
883558fd9c updates for java 25 compatibility 2026-01-28 13:47:33 +02:00
Craig Raw
9d576bc45d avoid scanning for legacy addresses on bitbox02 with wallet discovery 2026-01-23 09:34:19 +02:00
Craig Raw
49d807f31b fix trezor change detection on signing 2026-01-22 15:42:25 +02:00
Craig Raw
0c2ee6690f add connected device wallet discovery with configurable number of accounts to scan 2026-01-22 13:09:39 +02:00
Liz Lightning
85c81adc09
add user agent to coingecko historical rates call 2026-01-21 11:36:46 +02:00
Craig Raw
04de83706e improve qr encoding ui, save previous selection and add raw encoding for tx hex 2026-01-20 14:43:47 +02:00
Craig Raw
ab99f1d392 fix handling of non-standard key derivations when writing output descriptors 2026-01-15 10:32:40 +02:00
Craig Raw
53857389b7 use precise fee rates estimate from mempool.space 2026-01-15 09:48:22 +02:00
Craig Raw
98576d40c6 convert from tilepane to gridpane to resolve mnemonic words layout issue 2026-01-14 16:16:54 +02:00
Craig Raw
96f7d4bdb3 add additional fee rate limit check for high fee transactions 2026-01-13 16:30:57 +02:00
Michele Balistreri
0c67962730
add support for keycard via smart card interface 2026-01-13 15:11:54 +02:00
Craig Raw
0515d8056b add user agent to coingecko exchange source request 2026-01-13 14:49:25 +02:00
Craig Raw
cf15760d34 avoid extraneous error logging on card enumeration 2026-01-13 14:07:52 +02:00
Craig Raw
f28f15df66 remove unnecessary lark .gitmodules 2026-01-13 11:18:17 +02:00
Craig Raw
34900d2909 add any missing key path information to psbts once signing wallet is chosen 2026-01-03 14:21:44 +02:00
Craig Raw
430f2ee320 add warning when entering outdated slip132 values into a watch-only wallet of a different script type 2025-12-23 09:03:12 +02:00
Craig Raw
74160ba35f minor trezor v2 fixes 2025-12-20 10:41:35 +02:00
Craig Raw
59d85cdd05 improve thp pairing flow, and add passphrase session support 2025-12-18 13:12:54 +02:00
Craig Raw
0406421918 add trezor safe 7 noise config implementation 2025-12-17 14:21:38 +02:00
Craig Raw
4ddd09fe22 update to macos-15-intel runner 2025-12-11 11:59:52 +02:00
Craig Raw
a49edb7c45 add ledger nano gen5 support 2025-12-11 11:08:51 +02:00
Craig Raw
5275cab436 update drongo for bip93 package export 2025-12-09 12:05:55 +02:00
Ian McKenzie
4dcd463d92
add codex32 importer to software keystore import workflow 2025-12-09 12:04:35 +02:00
Craig Raw
f900f6dcb2 minor ui changes to master private key import 2025-12-04 10:24:45 +02:00
Craig Raw
b0ab6c9d42 ensure plugdev is added as a system group 2025-12-03 08:25:23 +02:00
Craig Raw
b8a703add2 improve calculation of taproot tweaked keys 2025-12-01 11:38:25 +02:00
Craig Raw
4185a8dced update drongo with bech32 refactor 2025-11-27 07:38:49 +02:00
Craig Raw
a608197038 followup to display address for watch-only wallets 2025-11-26 07:07:12 +02:00
Craig Raw
b4630043eb update block logo fill for dark theme 2025-11-25 10:38:48 +02:00
Liz Lightning
6452bf8184
add block.xyz augur fee estimator 2025-11-25 10:32:33 +02:00
Craig Raw
71cb5852b3 always show display address for watch-only wallets 2025-11-25 09:51:13 +02:00
Craig Raw
8efee4eaae pass default derivation to usb device and card import panes 2025-11-25 09:43:43 +02:00
Craig Raw
ac044c6f71 update drongo for bip32 tests 2025-11-24 11:44:27 +02:00
Craig Raw
d2d45e5432 improve psbt/tx matching and ensure incoming psbt signatures are always verified 2025-11-24 11:18:29 +02:00
Craig Raw
c16997eacc handle errors if silent payments psbt validation fails 2025-11-19 16:06:55 +02:00
Craig Raw
21543de007 minor updates to handle psbtv2 as the default internal representation 2025-11-19 11:57:58 +02:00
Craig Raw
6f772cb193 update to support bip375 2025-11-18 10:37:02 +02:00
Craig Raw
a4634f6a3d update drongo for keycard wallet models 2025-11-12 16:58:35 +02:00
Michele Balistreri
a794dafdf9
add keycard shell importers for singlesig and multisig 2025-11-12 16:57:38 +02:00
Craig Raw
9fb19ebe8b add bech32m tests 2025-11-12 11:12:51 +02:00
Craig Raw
46a39225bd update to configurable app name in codesign action for macos 2025-11-11 16:37:23 +02:00
Craig Raw
a0f7e2e65d use sparrowwallet action for macos codesigning 2025-11-10 12:50:22 +02:00
Craig Raw
39cb946e08 add frigate to detected electrum server capabilities list 2025-11-10 10:10:23 +02:00
Craig Raw
0894407b0b delete home folder on exit if empty 2025-11-07 08:10:13 +02:00
Craig Raw
c76acb8d82 bump to v2.3.2 2025-11-06 13:00:07 +02:00
Craig Raw
16e73ebc32 implement hide amounts feature 2025-11-06 10:00:07 +02:00
Craig Raw
3a5fa69fb6 fix occasional issue with cell reuse when avoiding updating cells during table size estimation 2025-11-05 12:20:02 +02:00
Craig Raw
4774830ce4 add yu12 to supported pixel formats on linux 2025-11-05 07:51:26 +02:00
Craig Raw
2f62a9e9c8 show signing keystores in transaction blockchain form for spends from multisig wallets 2025-11-04 10:45:31 +02:00
doblon8
75bcfe2253
update jzbar dependency to 0.2.1 2025-10-24 09:38:11 +02:00
Craig Raw
bedf1399ea request display of path when retreiving xpubs on ledger devices for any non-standard path 2025-10-24 08:38:48 +02:00
Craig Raw
58575793ea update openpnp-capture to 0.0.30-1 2025-10-21 15:55:32 +02:00
Craig Raw
6c9b580d4f refactor to use transaction parameters record object when creating a wallet transaction 2025-10-21 12:06:00 +02:00
doblon8
31909b7a15
use language-independent sid for windows users group permission 2025-10-21 09:35:18 +02:00
Craig Raw
092267339a adapt to use declarative style to for consolidation payments 2025-10-17 10:27:20 +02:00
Craig Raw
0974918cff hide confirmations in tooltip when showing inputs and outputs on the transactions table 2025-10-16 08:42:55 +02:00
Craig Raw
0f4c36b3c2 add ctrl+shift+left/right keyboard shortcuts for moving tabs left and right 2025-10-13 14:26:37 +02:00
Craig Raw
e1fe35fb74 update nsmenufx to avoid npe 2025-10-06 14:52:24 +02:00
Craig Raw
d37fd00c4b avoid using deprecated camera device type constants on recent macos versions 2025-10-06 13:27:12 +02:00
Craig Raw
5f54f86df7 bump to v2.3.1 2025-10-03 11:53:27 +02:00
Craig Raw
e2fa3df08d restore pre gradle 9 archive task behaviour for file permissions 2025-10-03 10:09:57 +02:00
Craig Raw
6d6ede9abe bump to v2.3.0 2025-10-03 08:08:25 +02:00
Craig Raw
cca9ab1056 improve implementation of adding dns payment information from psbt 2025-10-02 14:58:02 +02:00
Craig Raw
9e33861110 revert to javafx 23 due to jpackage launcher link bug 2025-10-01 12:10:39 +02:00
Craig Raw
c3d3fd1fda revert to java 22 and javafx 24 due to bug in jpackage launcher linking (jdk-8345810) 2025-10-01 11:52:45 +02:00
Craig Raw
ca8553ecb8 revert continuity camera device change as unsupported on macos 13 2025-09-30 12:33:33 +02:00
Craig Raw
d23ee8c086 upgrade openpnp-capture to iterate over continuity camera devices on mac 2025-09-30 12:24:22 +02:00
Craig Raw
e776a17ad4 upgrade jdbi to remove older caffeine dependency 2025-09-30 09:37:45 +02:00
Craig Raw
480ce1e476 fix deprecation warning 2025-09-29 17:39:48 +02:00
Craig Raw
656cd90b08 upgrade guava and commons-lang3 2025-09-29 14:34:07 +02:00
Craig Raw
8df0777959 upgrade to java 25 and javafx 25 2025-09-29 13:32:56 +02:00
Craig Raw
84566b92e6 remove unnecessary zbar native libraries 2025-09-29 12:39:49 +02:00
Craig Raw
7802510e58 support dns hrns in send to many dialog 2025-09-29 11:53:52 +02:00
Craig Raw
efb1eb1051 add initial sending to silent payments support 2025-09-29 08:37:07 +02:00
Craig Raw
6240667478 improve error dialog on payjoin receiver error 2025-09-02 09:49:39 +02:00
Craig Raw
2c27112dad update drongo 2025-08-16 13:03:03 +02:00
Craig Raw
6d53e1ed1d fix bluewallet spelling 2025-08-12 08:09:01 +02:00
Craig Raw
e8c5660897 allow transaction diagram input and output labels to expand into available width 2025-08-11 14:03:51 +02:00
Craig Raw
bef6c750bd upgrade to gradle 8.14.3 2025-08-07 11:28:27 +02:00
Craig Raw
4ec3603789 fix non bip32 child derivation test 2025-08-07 08:55:19 +02:00
Craig Raw
90c9f9733f display a warning if an output descriptor provided in the wallet settings will be modified for use 2025-08-05 09:28:52 +02:00
Craig Raw
64efcf67d3 display zero byte length witness elements as empty instead of op_0 2025-08-04 13:38:41 +02:00
Craig Raw
385d173948 handle npe connecting to bitcoin core with wallet functionality disabled 2025-08-01 07:44:34 +02:00
Craig Raw
d81b868049 add any dns payment instructions from loaded psbts if not already cached 2025-07-31 11:47:43 +02:00
Craig Raw
2ff7a15d1e add padding to writes when connected over tls 2025-07-31 10:34:56 +02:00
Craig Raw
f48fa7e23c support zero in pin keypad for onekey classic pin entry 2025-07-29 14:45:30 +02:00
Craig Raw
4632850e1e use improved dnssec validation and handle offline state when resolving bip 353 hrns 2025-07-29 12:52:10 +02:00
Craig Raw
5f62523710 support sending to and displaying bip353 human readable names and include dnssec proof in associated psbts 2025-07-24 14:36:11 +02:00
Craig Raw
9dcf210762 support creating transactions with the minimum relay fee rate configured by the user or set by the connected server 2025-07-17 09:15:59 +02:00
Craig Raw
c7e9a0a161 restore coingecko historical rate support by limiting to the last 365 days 2025-07-15 09:06:52 +02:00
Craig Raw
fa10714844 followup 2025-07-10 08:40:31 +02:00
Craig Raw
80105aee62 save webcam device unique id instead of name to config 2025-07-10 08:15:00 +02:00
Craig Raw
3c5fa58a16 suppress warnings for jzbar ffm usage 2025-07-09 12:46:57 +02:00
doblon8
2a2be2617c
replace jni-based zbar wrapper with ffm-based jzbar
* Replace ZBar JNI library implementation with jzbar

* Move ZBar.java to com.sparrowwallet.sparrow.control package

* Move ZBar.java to the com.sparrowwallet.sparrow.io package

* Switch to jzbar from Maven Central and update module/package imports accordingly

* Remove jzbar entry from extraJavaModuleInfo to avoid module patching error
2025-07-09 11:55:51 +02:00
Craig Raw
6c9a0d14cd compare on device unique id when choosing selected camera 2025-07-09 10:58:58 +02:00
Craig Raw
f82fcb58bb fix issue of including parent path elements in deterministic key when deriving child xpub from an output descriptor containing more than two child path elements 2025-07-09 10:26:49 +02:00
Craig Raw
5ec3bff6a4 fix jade configuration for signet and regtest networks 2025-07-02 16:45:10 +02:00
Oleg Koretsky
134dc826ba
do not change coin label unit on right click 2025-07-02 16:32:10 +02:00
Craig Raw
cd2a6623a4 fix restart menu options on linux standalone package 2025-07-01 16:07:45 +02:00
Craig Raw
49ab9e40e3 select first matching webcam by name 2025-06-28 16:10:57 +02:00
Craig Raw
cec7eac9ac fix selection of nearest supported resolution where chosen resolution is not available 2025-06-24 11:26:13 +02:00
Craig Raw
33e043fd9a include child derivations in output descriptor for bip 129 wallet export 2025-06-24 10:52:52 +02:00
Craig Raw
3aae26b196 bump to v2.2.4 2025-06-10 09:01:37 +02:00
Craig Raw
73d4fd5049 prevent double free when closing capture library 2025-06-09 14:43:06 +02:00
Craig Raw
a94380e882 minor specter diy ui tweaks 2025-06-07 11:23:06 +02:00
Craig Raw
e4dd4950bf prevent selection of unsupported bip322 format when signing a message with a connected device 2025-06-06 13:07:36 +02:00
Craig Raw
26ce1b3469 derive to maximum bip32 account level where child path in output descriptor contains more than two elements 2025-06-06 11:45:46 +02:00
Craig Raw
ebce34f3d1 minor tweaks 2025-06-05 14:23:02 +02:00
Craig Raw
f28e00b97e suggest opening the send to many dialog when adding multiple payments on the send tab 2025-06-05 10:31:37 +02:00
Craig Raw
25770c2426 suggest connecting to broadcast a finalized transaction if offlineand a server is configured 2025-06-05 09:40:17 +02:00
Craig Raw
799cac7b1f handle bitkey descriptor export format 2025-06-05 08:28:21 +02:00
Craig Raw
c265fd1969 fix cormorant server.version rpc issue 2025-06-04 17:18:31 +02:00
Craig Raw
890f0476b1 introduce delay before closing capture library 2025-06-04 15:23:48 +02:00
Craig Raw
4d93381124 improve electrum server script hash unsubscribe support 2025-06-04 14:52:33 +02:00
Craig Raw
364909cfa3 support nv12 capture pixel format on linux 2025-06-03 12:48:01 +02:00
Craig Raw
38f0068411 detect if electrum server supports scripthash unsubscribe capability 2025-06-03 12:38:03 +02:00
Craig Raw
8885e48ed9 request rgb3 pixel format on linux where returned format is unsupported 2025-06-02 16:28:44 +02:00
Craig Raw
31ce3ce68a further electrum server optimisations 2025-06-02 15:56:46 +02:00
Craig Raw
b0d0514617 remove possibility of task queueing in webcam service 2025-06-02 11:36:06 +02:00
Craig Raw
d7d23f9b58 always use the master wallet payment code when creating the notification transaction payload on the send tab 2025-06-02 09:41:46 +02:00
Craig Raw
3fdf093a26 use semaphore to ensure last webcam service task has completed before closing stream 2025-05-29 14:17:58 +02:00
Craig Raw
74c298fd93 iterate and remove faulty capture devices on opening qr scan dialog 2025-05-29 13:58:46 +02:00
Craig Raw
4298bfb053 bump to v2.2.3 2025-05-22 14:58:09 +02:00
Craig Raw
231eb13cee retrieve and show next block median fee rate in recent blocks view where available 2025-05-22 13:35:59 +02:00
Craig Raw
52470ee6d8 further electrum server optimization tweaks 2025-05-22 11:59:25 +02:00
Craig Raw
853949675e fix npe configuring recent blocks view on new installs 2025-05-22 08:44:39 +02:00
Craig Raw
098afebbe0 increase recent blocks estimated fee rate update frequency 2025-05-21 15:38:06 +02:00
Craig Raw
63c0a6d6e2 bump to v2.2.2 2025-05-21 13:29:16 +02:00
Craig Raw
77c305f90b tweak fix on recent blocks view 2025-05-21 10:29:01 +02:00
Craig Raw
276f8b4148 fix npe on null fee returned from server 2025-05-21 10:12:38 +02:00
Craig Raw
b3c92617c9 minor fixes on recent blocks view 2025-05-21 10:05:58 +02:00
Craig Raw
58635801fc add icons for external sources in settings and recent blocks view 2025-05-21 09:55:22 +02:00
Craig Raw
8c32bb3903 followup 2025-05-20 19:45:43 +02:00
Craig Raw
55a2c86a83 upgrade tor resource to fix uuid issue on recent macos platforms 2025-05-20 19:40:16 +02:00
Craig Raw
345e018eb9 repackage .deb installs to use older gzip instead of zstd compression 2025-05-20 13:41:12 +02:00
Craig Raw
45d2dee764 remove display of median fee rate where fee rates source is set to server 2025-05-20 12:04:06 +02:00
Craig Raw
250bc84060 bump to v2.2.1 2025-05-20 10:58:21 +02:00
Craig Raw
c3dba8ede6 bump to v2.2.0 2025-05-19 11:37:52 +02:00
Craig Raw
db478f8da6 further followup tweaks 2025-05-19 09:11:12 +02:00
Craig Raw
4ab9a9f681 followup tweaks 2025-05-16 18:49:58 +02:00
Craig Raw
c078aea3b4 show total in transaction diagram when constructing multiple payment transactions 2025-05-16 17:00:40 +02:00
Craig Raw
af4a283b3f increase trezor device libusb timeout 2025-05-16 10:02:03 +02:00
Craig Raw
892885c0b1 make wallet summary table grow horizontally with dialog sizing 2025-05-15 15:01:09 +02:00
Craig Raw
d4a1441d65 more recent blocks tweaks 2025-05-15 12:42:53 +02:00
Craig Raw
1605cd2619 followup 2025-05-15 12:10:54 +02:00
Craig Raw
b4d34aacc5 tweak block cube median fee font styling 2025-05-15 12:03:24 +02:00
Craig Raw
1a4f0113c7 followup 2 2025-05-15 10:49:09 +02:00
Craig Raw
055e3ac496 followup 2025-05-15 10:00:03 +02:00
Craig Raw
d0da85171c rename sparrow package to sparrowwallet and sparrowserver on linux 2025-05-15 09:28:42 +02:00
Craig Raw
af4c68a09c update tor resource library and switch to resource-filterjar plugin 2025-05-14 11:38:17 +02:00
Craig Raw
b1ab157ee3 cormorant: add block stats rpc call, and prefer for block summaries 2025-05-14 10:52:21 +02:00
Craig Raw
94b27ba7e8 add recent blocks view 2025-05-14 08:19:21 +02:00
Craig Raw
e697313259 add accessible text to improve screen reader navigation 2025-05-08 10:10:50 +02:00
Craig Raw
1b0e5e9726 revert rpm package name change 2025-05-07 16:05:17 +02:00
Craig Raw
df0c4310ca optimize and reduce electrum server rpc calls #3 2025-05-07 16:03:02 +02:00
Craig Raw
474f3a4e91 add custom filterjar plugin to filter out unneeded native binaries per platform 2025-05-07 10:37:56 +02:00
Craig Raw
c6e42d8fe2 rename rpm package name from sparrow to sparrowwallet to avoid conflicts 2025-05-05 15:11:04 +02:00
Craig Raw
3698ca8e85 reduce tooltip show delay to 200ms 2025-05-05 14:53:24 +02:00
Craig Raw
53c5a8d2df update kmp-tor to 2.2.1 and remove runtime module config 2025-05-05 14:51:15 +02:00
Craig Raw
3d85491e6b add block summary service 2025-05-05 14:43:42 +02:00
Craig Raw
c77f52f7f6 optimize and reduce electrum server rpc calls #2 2025-04-29 12:49:58 +02:00
Craig Raw
e3138f3392 optimize and reduce electrum server rpc calls 2025-04-28 14:39:30 +02:00
Craig Raw
7a4015fdb5 convert images to theme aware svg for all wallet models and dialogs 2025-04-25 17:37:32 +02:00
Craig Raw
94d15c09e6 cormorant: avoid calling listwalletdir rpc on initialization due to extremely slow response on windows 2025-04-18 09:43:57 +02:00
Craig Raw
71ac72e9f6 upgrade internal tor to 0.4.8.15 2025-04-17 14:17:26 +02:00
Craig Raw
be8b56e355 fix inclusion of fees on wallet label exports 2025-04-14 16:27:12 +02:00
Craig Raw
af8505c0eb support send and display of pay to anchor outputs 2025-04-14 15:50:10 +02:00
Craig Raw
5edabf2e14 minor fixes to private key sweep on bitcoin core 2025-04-11 13:43:19 +02:00
Craig Raw
c73ebdc8a2 show address where available on input and output tooltips in transaction tab tree 2025-04-10 17:01:18 +02:00
Craig Raw
c9d7b8ef9a dynamically truncate input and output labels in the tree on a transaction tab, and add tooltips if necessary 2025-04-10 15:42:04 +02:00
Craig Raw
b3a6340c45 simplify camera pixel format prioritisation 2025-04-08 14:50:38 +02:00
Craig Raw
0975d12155 sort camera pixel formats on linux only 2025-04-08 13:48:59 +02:00
Craig Raw
e31aa7fc80 avoid server address resolution for public servers, and assume non local for failures where a proxy is configured 2025-04-06 20:55:35 +02:00
Craig Raw
b777c8c64d fix for building on headless with earlier javafx 2025-04-03 16:11:23 +02:00
Craig Raw
4176f76ffc update to build on ubuntu 22.04 2025-04-03 15:59:07 +02:00
Craig Raw
64dac72f4f show transaction diagram fee percentage as less than 0.01% rather than 0.00% 2025-04-03 15:35:51 +02:00
Craig Raw
e29559f59c fix issue parsing remote coldcard xpub encoded on a different network 2025-04-03 15:18:42 +02:00
Craig Raw
b1223ef064 reset preferred table column widths on adjustment 2025-04-03 14:41:43 +02:00
Craig Raw
6f0a30cc25 prefer yuyv to mjpg capture format 2025-04-03 14:15:19 +02:00
Craig Raw
2fa8e5fd70 improve tabs and transaction diagram tooltips with long labels 2025-04-03 14:10:02 +02:00
Craig Raw
a8f7ce9e34 add tooltip for truncated labels in table cells 2025-04-02 10:32:11 +02:00
Craig Raw
c946ef7479 upgrade bouncy castle, pgpainless and logback 2025-04-01 14:59:55 +02:00
Craig Raw
7fa13901d4 fix typo 2025-03-20 12:07:07 +02:00
Craig Raw
8a88488a42 update openpnp-capture to v0.0.28-5, fix typo 2025-03-20 11:48:22 +02:00
Craig Raw
25a3f5539d sort retrieved capture formats in order of supported, unknown and unsupported pixel formats 2025-03-20 10:47:07 +02:00
Craig Raw
520c5f2cfa revert initialization change, configure openpnp debug logging 2025-03-14 11:27:25 +02:00
Craig Raw
d8877a259c initialize capture library in service thread, fix sigsegv fault 2025-03-14 09:40:30 +02:00
Craig Raw
7de63b2b5f suppress unneeded warning on zoom detection 2025-03-13 17:55:49 +02:00
Craig Raw
f1c4b8aa69 support camera zoom during capture with mouse scroll 2025-03-13 17:43:55 +02:00
Craig Raw
6f6d61fb75 minor webcam cross platform fixes 2025-03-13 16:54:47 +02:00
Craig Raw
2c4de99fad improve capture display efficiency, fix resizing bug and refactor 2025-03-13 13:52:01 +02:00
Craig Raw
3e197eb310 support capturing using additional webcam resolutions of fhd and uhd4k 2025-03-13 08:30:53 +02:00
Craig Raw
bd5af560ff fix non-zero account script type detection when signing a message on trezor devices 2025-03-12 08:52:08 +02:00
Craig Raw
3b9551a8c6 replace sarxos/openimaj library with openpnp-capture library 2025-03-11 16:21:27 +02:00
Craig Raw
289a4453a4 fix issue with random ordering of keystore origins on labels import 2025-03-10 11:38:26 +02:00
Craig Raw
27e21c890f refactor ioutils to drongo 2025-03-04 15:08:26 +02:00
Craig Raw
4239a56bc1 show warning when importing a wallet with a derivation path matching another script type 2025-03-04 11:48:03 +02:00
Craig Raw
5c9de07d48 prefer verifying dropped file over default file where file is not in manifest 2025-03-03 14:17:53 +02:00
Craig Raw
9a8a25344a bump to v2.1.4 2025-02-27 12:51:30 +02:00
Craig Raw
be86b4feaa fix access issue with macos show/hide windowing commands 2025-02-27 11:08:01 +02:00
Craig Raw
37763e9557 verify dropped release file instead of first platform specific release file found 2025-02-27 11:03:53 +02:00
Craig Raw
80c4f4f5f6 make wallet labels export and import scannable 2025-02-26 12:01:05 +02:00
Craig Raw
6c3fe93d1e exclude heights of confirming txes from wallet labels export 2025-02-26 11:44:27 +02:00
Craig Raw
76eff2de48 merge wallet labels optional fields draft implementation 2025-02-26 10:47:17 +02:00
Craig Raw
07a6818823 use default key origin information when importing a descriptor without key origin info 2025-02-25 10:55:37 +02:00
Craig Raw
2253a1bb97 add support for onekey pro and classic 1s hardware wallets 2025-02-20 17:04:56 +02:00
Craig Raw
36ee8add08 add bip47 notification transaction test 2025-02-19 11:31:32 +02:00
Craig Raw
883e75c0df add copy payment code to transaction diagram outputs context menu 2025-02-19 08:45:52 +02:00
Craig Raw
cc908b09c7 upgrade to libusb 1.0.27 on all platforms 2025-02-18 15:30:34 +02:00
Craig Raw
ce963ed5b6 add specific handling for invalid windows device drivers on trezor devices 2025-02-18 13:32:02 +02:00
Craig Raw
951e33dc06 fix handling of high account numbers on ledger devices 2025-02-18 12:45:50 +02:00
Craig Raw
6a6a6b1cca additionally check for trezor model against internal name, improve exception handling on no match 2025-02-16 08:43:45 +02:00
Craig Raw
8953d404fa fix stripping leading zeros from master fingerprint on importing some trezor models 2025-02-14 18:42:44 +02:00
Thauan Amorim
b366177782
add show transaction as qr button to signed transaction tab when offline
* [feature/1630] Add QR code button on signed transaction screen

* [feature/1630] Button positioning improvements

* [feature/1630] Added owner to qrDisplayDialog
2025-02-13 09:05:11 +02:00
Craig Raw
d0c827c2c7 fix various minor issues around multisig keystore labelling and export button visibility 2025-02-13 08:43:55 +02:00
Craig Raw
5c29bf51b7 handle scanning and pasting server urls in the electrum format 2025-02-11 14:03:43 +02:00
Craig Raw
d426703dcc fix account discovery on bitbox02 2025-02-11 13:18:21 +02:00
Craig Raw
78f0721168 bump to v2.1.3 2025-02-08 16:12:38 +02:00
Craig Raw
20d3f07059 draft implementation of optional bip329 fields 2025-02-08 11:43:59 +02:00
Craig Raw
1140a678ad followup 2025-02-08 11:38:41 +02:00
Craig Raw
6e8d44bc8c set script type import button as default after bip39 wallet discovery returns empty 2025-02-08 09:44:21 +02:00
Craig Raw
ad3b384feb fix loading hex txn files with trailing whitespace 2025-02-08 09:30:21 +02:00
Craig Raw
f38350b38d prefill derivation to default path for script type on watch only keystores 2025-02-07 16:22:20 +02:00
Craig Raw
62060c9839 fix unsigned byte to int conversion for ledger get_more_elements command 2025-02-07 15:56:35 +02:00
Craig Raw
8975f6f666 followup 2025-02-07 14:57:21 +02:00
Craig Raw
c7351cd191 indicate historical rates support in exchange source drop-down 2025-02-07 14:52:17 +02:00
Craig Raw
62b1dc3900 support coldcard p2tr address display and show correct address for script type on message sign 2025-02-06 16:12:49 +02:00
Craig Raw
f37ff47850 fix invalid claimed length error on transaction file load 2025-02-06 15:36:37 +02:00
Craig Raw
cfaa1f6c6e add local network usage description for macos 15 2025-02-06 10:08:28 +02:00
Craig Raw
91c94b94eb upgrade flyway to v9.22.3 2025-02-06 09:37:51 +02:00
Craig Raw
a5eb7da067 bump to v2.1.2 2025-02-05 09:21:21 +02:00
Craig Raw
195dbcef3b ensure /sys devices are writeable before calling udevadm 2025-02-04 20:53:50 +02:00
Craig Raw
24955604e2 use standard font in label cells on macos 2025-02-04 20:16:28 +02:00
Craig Raw
0305afbc02 catch and log any linkage errors while enumerating hwws 2025-02-04 19:56:07 +02:00
Craig Raw
d4c3c3afa8 bump to v2.1.1 2025-02-04 13:58:47 +02:00
Craig Raw
cda7835672 revert rpm spec file to use %post 2025-02-04 11:45:53 +02:00
Craig Raw
b4b679dd16 fix rpm spec path reference 2025-02-04 11:22:06 +02:00
Craig Raw
3efaec2859 verbose rpm build 2025-02-04 11:14:06 +02:00
Craig Raw
a53812c12f improve rpm spec and deb postinst scripts 2025-02-04 11:07:30 +02:00
Craig Raw
686c008e97 allow server urls to be pasted into the server settings host fields 2025-02-03 18:02:18 +02:00
Craig Raw
4d60a20336 add mempool.space exchange rate source 2025-02-03 14:47:23 +02:00
Craig Raw
9879889875 bump to v2.1.0 2025-02-03 08:56:11 +02:00
Craig Raw
4aee89a35b fix trezor one passphrase protection deactivation 2025-01-31 15:01:32 +02:00
Craig Raw
fd9648efd1 fix trezor one passphrase behaviour, add bitbox02 non-standard path check 2025-01-31 13:50:54 +02:00
Craig Raw
8f438cd0bc rename preferences to settings 2025-01-31 11:41:20 +02:00
Craig Raw
8b47701dbe check for and delete hwi directory on macos and windows 2025-01-31 11:04:40 +02:00
Craig Raw
ff571c3df4 ensure consistent key ordering in ledger multisig wallet policy 2025-01-30 16:19:49 +02:00
Craig Raw
201c9ccca3 truncate labels to persistable max label length and notify user via tooltip 2025-01-30 14:50:46 +02:00
Craig Raw
cbae280cc8 add jade plus to udev rules 2025-01-30 12:05:08 +02:00
Craig Raw
1f44229e62 fix rpm spec file 2025-01-29 14:34:05 +02:00
Craig Raw
541c71a002 verbose build 2025-01-29 13:10:29 +02:00
Craig Raw
bb08b5599c install rpmbuild on ubuntu arm64 2025-01-29 12:50:32 +02:00
Craig Raw
fd70f03259 add arm64 runners 2025-01-29 12:36:43 +02:00
Craig Raw
fbca6c691d add blackie.c3-soft.com testnet4 public electrum server 2025-01-28 13:24:10 +02:00
Craig Raw
e8cd56388f upgrade to javafx 23.0.2 2025-01-28 13:20:31 +02:00
Craig Raw
3dfd8210a8 store treetable column sort on adjustment, and restore on wallet load 2025-01-28 12:53:10 +02:00
Craig Raw
f9199b65f0 store treetable column widths on adjustment, and restore on wallet load 2025-01-28 10:33:46 +02:00
Craig Raw
6a001bd67f exclude taproot wallets and jade, tapsigner and satochip hwws from requiring non witness tx in psbts 2025-01-23 15:40:44 +02:00
Craig Raw
ee2f387cd5 retrieve, store and use device registrations to avoid unncessary reregistration on ledger multisig wallets 2025-01-22 16:26:42 +02:00
Craig Raw
95200c7143 improve bitbox pairing flow 2025-01-22 12:59:23 +02:00
Craig Raw
d7511c62bf match new behaviour in bitcoin core 28 for default windows data dir 2025-01-22 09:49:50 +02:00
Craig Raw
7a5f4ff294 reduce default tooltip show delay to 400ms 2025-01-22 07:51:04 +02:00
Craig Raw
2b145cb9cc update install udev rules dialog 2025-01-21 13:55:45 +02:00
Craig Raw
13bd05853c add udev rules installation 2025-01-21 13:11:19 +02:00
Craig Raw
59f3338842 fix coldcard last error check 2 2025-01-21 11:34:10 +02:00
Craig Raw
2cc38dc8b0 fix coldcard last error check 2025-01-21 10:41:42 +02:00
Craig Raw
0e9d97c221 replace hwi with lark 2025-01-21 10:00:38 +02:00
Craig Raw
fb0fd013d9 add lark as submodule 2025-01-20 09:49:01 +02:00
Craig Raw
e7510d2275 rename max block size constant for clarity 2025-01-20 09:17:05 +02:00
Craig Raw
e92d0f9b58 show input label in input tooltip on transaction diagram if present 2025-01-16 14:21:42 +02:00
Craig Raw
ea23bb51d9 upgrade lanterna and remove java 22 workaround 2025-01-16 12:51:10 +02:00
Craig Raw
2d3bf0b2fe skip labelled addresses when retrieving an unused address from the receive tab and send tab pay to wallet 2025-01-16 12:18:12 +02:00
Craig Raw
617ad380c0 improve socket address resolution handling 2025-01-15 15:22:15 +02:00
Craig Raw
29ac15846d disable broadcast progress bar if disconnected, and re-enable if connected again 2025-01-15 13:07:18 +02:00
Craig Raw
f4acd3e587 add option to bitcoin core and private electrum server selection to scan url from a qr code 2025-01-15 11:59:45 +02:00
Craig Raw
f057b92729 allow camera image mirroring to be changed from image context menu and application view menu 2025-01-15 11:07:45 +02:00
Craig Raw
4bf02f833c remove payjoin verification step to check there is no previous utxo information in the psbt as per bip78 change 2025-01-15 09:26:48 +02:00
craigraw
7ef51e6a5d
Merge pull request #1591 from Toporin/patch-satochip-multisig
default to first keystore for signing path if satochip keystore cannot be determined
2025-01-15 09:14:06 +02:00
Craig Raw
fdbcea1625 enable electrum rpc batching on mempool-electrs servers 2025-01-15 09:09:25 +02:00
Craig Raw
218c2720e0 always select a new address when sending multiple payments to the same open wallet 2025-01-15 08:21:48 +02:00
Toporin
91ad82a21c Patch https://github.com/Toporin/SatochipApplet/issues/15
First try to recover derivation path from satochip keystore, otherwise from first keystore as default value.
2025-01-14 15:23:38 +01:00
Toporin
f4b3b3d55a Merge branch 'master' into patch-satochip-multisig 2025-01-14 15:01:35 +01:00
Craig Raw
db1b55cfa0 cormorant: report configuration error when both core data folder and user/pass is not specified 2025-01-14 15:12:39 +02:00
Craig Raw
bd0aca66b5 cormorant: skip waiting for ibd to complete when networkactive is false 2025-01-14 14:15:44 +02:00
Toporin
22ad1cc5d1 Patch https://github.com/Toporin/SatochipApplet/issues/15
Null exception can be thrown when signing a multisig transaction
from a Sparrow wallet reconstructed from a Bitcoin descriptor.
This happens when the user did not configure any keystore
with the corresponding Satochip card ('import' button).
In this case, the 'fullpath' derivation path remains undefined,
leading to the exception.
2025-01-14 13:00:52 +01:00
Craig Raw
d07a5f0a01 cormorant: add fee to mempool tx entries returned from get history 2025-01-14 12:23:18 +02:00
Craig Raw
947013e088 only show cpfp rate if child fee increases effective fee rate 2025-01-14 10:44:53 +02:00
Craig Raw
25f441a6a8 update javafx to 23.0.1 2025-01-13 10:22:54 +02:00
Craig Raw
ee5015f0d5 update macos runner 2024-12-17 12:17:00 +02:00
Craig Raw
4f00fabd23 upgrade tern to 1.0.6 2024-11-28 11:03:49 +02:00
Craig Raw
6927423d68 switch from controlsfx platform to drongo ostype 2024-11-26 11:30:32 +02:00
Craig Raw
fffa636956 followup 2024-11-26 11:00:26 +02:00
Craig Raw
a02ac3dcd2 use versionless extra module info definitions where possible 2024-11-26 10:49:38 +02:00
Craig Raw
e56e3d9394 switch from custom to gradlex extra-java-module-info plugin, cleanup module definitions 2024-11-26 09:31:34 +02:00
Craig Raw
119d00233d fix cast to http proxy supplier 2024-11-25 16:34:17 +02:00
Craig Raw
da427610d6 move version class to drongo 2024-11-25 15:53:27 +02:00
Craig Raw
46034b8f11 repackage http client as tern library dependency 2024-11-25 13:17:39 +02:00
Craig Raw
d49d5967b2 improve exception handling when loading paynym avatars 2024-11-25 10:30:28 +02:00
Craig Raw
484ef5f399 upgrade jcommander to 2.0 2024-11-20 13:09:28 +02:00
Craig Raw
740c00d1ba add output descriptor accessors and copy function 2024-11-19 10:46:48 +02:00
Craig Raw
dfae39255e add equals and hashcode to output descriptor 2024-11-18 15:14:40 +02:00
Craig Raw
c2bce893db fix psbtv2 output amount serialization 2024-11-18 13:05:41 +02:00
Craig Raw
ef063fde75 reverse prevtxid byte ordering during serialization and deserialization 2024-11-18 12:44:14 +02:00
craigraw
adb446de3e
Merge pull request #1537 from ottosch/hotkey-close-dialog
close wallet name dialog with escape key
2024-11-18 08:22:40 +01:00
ottosch
d040f186a2 Close wallet name dialog with ESC 2024-11-15 18:28:52 -03:00
Craig Raw
b4f9c52413 bump required java version to 22 2024-11-15 23:06:48 +02:00
Craig Raw
7527dd0909 allow hardened character selection when writing key 2024-11-15 16:33:10 +02:00
Craig Raw
b0be8ca7c2 add psbt v2 support 2024-11-15 12:34:46 +02:00
Craig Raw
1e0c0c1c75 replace forward slash with underscore in file names when saving psbts 2024-11-12 08:48:50 +02:00
Craig Raw
d731f7296b improve jade qr keystore import descriptions 2024-11-12 08:26:07 +02:00
Craig Raw
12034a07d7 add specter diy multisig option to wallet import menu 2024-11-05 08:49:36 +02:00
Craig Raw
60e3d4e107 be more lenient in parsing pasted btc values to send tab textfields 2024-11-04 08:03:21 +02:00
Craig Raw
ad8e17a3a0 add eckey arithmetic functions 2024-10-31 17:02:42 +02:00
Craig Raw
3e676eadcb add support for x25519 and secp256r1 keys 2024-10-30 13:04:35 +02:00
Craig Raw
3640db3d3d simplify required maven build repositories 2024-10-28 13:27:44 +02:00
Craig Raw
d0bf55de70 fix regression to display tabular numbers in a monospace font 2024-10-28 10:04:33 +02:00
Craig Raw
ad0b6adfd8 upgrade hummingbird to v1.7.4 2024-10-28 09:48:11 +02:00
Craig Raw
92b32b0d99 drongo: fix build instructions 2024-10-21 09:28:52 +02:00
Craig Raw
233addc1b7 update fxsvgimage to v1.1 2024-10-10 09:07:12 +02:00
Craig Raw
1d8c37066e update flyway to v9.1.3 2024-10-10 09:04:01 +02:00
Craig Raw
c450efe499 improve keystore import panel spacing in linux 2024-10-08 10:32:36 +02:00
craigraw
34bcc87468
Merge pull request #1512 from dcavacec/fix-issue-1510
improve handling of spacing and links in accordion panels
2024-10-08 10:24:59 +02:00
David Cavaceci
2aac365039 PR #1510 Feedback: set min height, use AppServices url handling 2024-10-07 09:59:58 -05:00
Craig Raw
7e68ecffd3 retrieve fee rates from configured source on non-mainnet networks where available 2024-10-07 12:13:24 +02:00
David Cavaceci
bf457620db Fix #1510: Handle spacing and links in content box messages. 2024-10-02 11:30:06 -05:00
Craig Raw
e50fe4c68c switch from paynym.is to paynym.rs and tor equivalents, update child wallet labels on displaying paynym dialog 2024-09-30 11:31:55 +02:00
Craig Raw
1bbc586cd6 set transaction tab label to transaction label if available 2024-09-24 08:49:04 +02:00
Craig Raw
e1dab3a48e update compress and jackson libs 2024-09-20 11:00:28 +02:00
Craig Raw
73b672a7ef fix arm64 architecture on server deb control file 2024-09-20 10:20:07 +02:00
Craig Raw
b142c54c68 update readme for java 22 2024-09-18 15:09:04 +02:00
Craig Raw
58d09c3ba7 bump to v2.0.1 2024-09-18 14:57:23 +02:00
Craig Raw
d5a7a5b855 update reproducible build instructions for java 22 2024-09-18 14:56:06 +02:00
Craig Raw
fcb83f8bfa bump to v2.0.0 2024-09-18 13:36:46 +02:00
Craig Raw
f187603ec3 upgrade to hwi 3.1.0 2024-09-18 09:23:30 +02:00
Craig Raw
8d7308bc37 add warning when sighash none is selected 2024-09-16 08:27:29 +02:00
Craig Raw
e44d1393f5 delegate to wallet model usb support 2024-09-13 13:13:49 +02:00
Craig Raw
33ba472843 set minimum fee rate to the lower of estimated and user configured fee rates 2024-09-13 13:04:04 +02:00
Craig Raw
faa81f2273 replace message after comparison check with that provided in signed file 2024-09-13 09:49:24 +02:00
Craig Raw
0646c8aa28 show warning dialog on broadcast if a transaction has a fee rate beyond the range slider maximum 2024-09-13 09:30:58 +02:00
Craig Raw
deb47ca002 truncate loading log and avoid automatic scrolling to the right 2024-09-12 14:30:05 +02:00
Craig Raw
ec131bb8da delay opening new dialogs on startup in wayland 2024-09-11 12:03:13 +02:00
Craig Raw
31f287125f delay show password dialog until initial app window open has completed 2024-09-06 13:04:22 +02:00
Craig Raw
e131f645f6 followup 2024-09-05 12:01:12 +02:00
Craig Raw
eabc0da6d5 specify deb control file when building headless to restrict dependencies 2024-09-04 15:11:51 +02:00
Craig Raw
49573d1075 upgrade to javafx 22 with a minimum requirement of macos 11 and gtk3 2024-09-04 12:04:00 +02:00
Craig Raw
17093dbf72 add menu items to the message sign dialog to save a text file for signing, and load a signed message file 2024-09-03 12:03:53 +02:00
Craig Raw
c2b5b24702 add passport multisig to wallet import menu 2024-09-02 12:40:54 +02:00
Craig Raw
65f1fa7cf8 remove oxt.me as fee rates source 2024-08-26 11:34:31 +02:00
Craig Raw
cbee341544 use monospace font for addresses in utxo table 2024-08-22 12:01:57 +02:00
Craig Raw
95b1aa8e48 rewrite derivation paths on file and card imports, compare multisig keystore derivations with rewritten paths 2024-08-22 11:07:29 +02:00
Craig Raw
af89be96e5 show warning if data is too large for display as static qr 2024-08-21 09:09:08 +02:00
Craig Raw
fad960c192 terminal: restore pre java 22 behaviour for system.console call 2024-08-20 15:18:44 +02:00
Craig Raw
1adeef04db preserve file timestamps on macos build zip 2024-08-20 13:14:44 +02:00
Craig Raw
47f925b677 use uri instead of deprecated url constructor 2024-08-09 10:24:44 +02:00
Craig Raw
5db3096386 upgrade java to 22.0.2 2024-08-09 09:45:23 +02:00
Craig Raw
62e98b0090 change windows installer from exe to msi 2024-08-09 09:44:33 +02:00
Craig Raw
76490604ac upgrade to gradle 8.9 2024-08-08 13:30:39 +02:00
Craig Raw
783733b9d3 followup 2024-08-07 14:56:29 +02:00
Craig Raw
041b5aa34c recover slip39 shares to keystore seed and store as single slip39 share 2024-08-07 14:45:09 +02:00
Craig Raw
33d23e9ea5 Merge branch 'master' of github.com:sparrowwallet/sparrow 2024-07-31 15:16:00 +02:00
Craig Raw
b3f6cc88f0 add trezor safe 5 support (hwi update still required) 2024-07-31 15:13:45 +02:00
craigraw
b912aa2eb9
Merge pull request #1437 from BenWestgate/1436-single-desktop-window
avoid adding a new window menu command on linux desktop managers
2024-07-22 13:41:24 +02:00
Craig Raw
d894343457 show a warning dialog before refreshing a passphrase wallet where all the history has changed 2024-07-17 12:22:55 +02:00
Craig Raw
fb1e1cefda upgrade zbar to v0.23.93 2024-07-16 13:16:49 +02:00
Craig Raw
d960bdce96 include multisig threshold and psbt keytype fixes 2024-07-11 11:49:17 +02:00
Craig Raw
fb679c0199 enable close button on multisig backup dialog 2024-06-29 10:11:24 +02:00
Ben Westgate
9ecfe0e94f
Add SingleMainWindow=true to Sparrow.desktop
This prevents desktop environments from displaying "New Window" as one of the right click actions in the side bar and application list.
2024-06-08 13:47:48 -05:00
Craig Raw
1bc2f7d69f add missing previous outputs to a loaded psbt if available from open wallets 2024-06-01 09:43:48 +02:00
Craig Raw
6e4d308c79 add optional bbqr selection for qr display on krux wallets 2024-05-29 09:26:14 +02:00
Craig Raw
afb6037449 show warning when sweeping a private key that contains insufficient funds for the given fee rate 2024-05-27 09:48:15 +02:00
Craig Raw
369983748d bump to v1.9.2 2024-05-14 11:36:19 +02:00
Craig Raw
0d16c87b40 minor caravan name update 2024-05-13 11:36:04 +02:00
Craig Raw
b59a65dcfe export electrum wallets with only usb capable hardware wallets as hardware keystore types 2024-05-10 09:54:33 +02:00
Craig Raw
87cc28e0a4 improve error message when importing invalid coldcard multisig config 2024-05-09 15:44:47 +02:00
Craig Raw
1187925543 fix wallet loading failure icon color in tab label when using dark theme 2024-05-09 14:42:13 +02:00
Craig Raw
cd4edab4ae add testnet4 public server and tx broadcast from mempool.space 2024-05-09 13:00:11 +02:00
Craig Raw
daf320f36b optionally show output descriptor qr export as bbqr, update coldcard import and export instructions 2024-05-09 12:20:54 +02:00
Craig Raw
f6ff92865b avoid adding testnet symlink in windows as admin privileges are required 2024-05-08 16:15:31 +02:00
Craig Raw
d420c71673 add testnet4 network support 2024-05-08 15:50:15 +02:00
Craig Raw
07101b3ca0 add additional fingerprint check when finding signing nodes from provided psbt input derivation paths 2024-05-06 09:51:34 +02:00
Craig Raw
00f7f3f5b3 update default derivation path for unknown unchained signer 2024-05-06 08:17:04 +02:00
Craig Raw
5d2c133401 fix single character multisig output descriptor threshold parsing issue 2024-05-03 12:16:25 +02:00
Craig Raw
7b0dfd66a7 fix premature decompression of bbqr zlib parts 2024-05-03 11:46:33 +02:00
Craig Raw
83719e7df2 fix signing regression on psbts with external inputs 2024-04-30 11:55:28 +02:00
Craig Raw
f1b246f0b0 bump to v1.9.1 2024-04-26 09:07:37 +02:00
Craig Raw
599880ea5c improve samourai backup import error message 2024-04-25 15:21:13 +02:00
Craig Raw
d625bab02e bump to v1.9.0 2024-04-25 15:13:10 +02:00
Craig Raw
1676676e06 remove whirlpool and soroban features and dependencies 2024-04-25 15:11:22 +02:00
Craig Raw
f7e603118f bump to v1.8.7 2024-04-24 11:27:58 +02:00
Craig Raw
f6fd889712 ignore scroll events with zero scroll movement 2024-04-24 09:00:36 +02:00
Craig Raw
21d91d3d10 add additional check against existing child wallet names when suggesting new accounts to add 2024-04-23 08:32:58 +02:00
Craig Raw
f1cddc28e7 copy context menu changes on receive tab 2024-04-22 16:57:44 +02:00
Craig Raw
1887e1c7b0 allow editing of the output descriptor of a new account on a watch only wallet 2024-04-22 13:29:41 +02:00
Craig Raw
3e870f362d fall back to sparrow logo for faulty wallet icon loads 2024-04-22 12:18:28 +02:00
Craig Raw
665d70b845 fix freeze on account settings tab loading wallet type icon 2024-04-22 12:05:24 +02:00
Craig Raw
c2cbe62a5a fix showing multiple password dialogs on bip47 paynym wallet linking 2024-04-22 10:02:02 +02:00
Craig Raw
c6b6e74515 maintain strong reference to key derivation service until action completion 2024-04-19 10:49:42 +02:00
Craig Raw
8ddc494b53 parse output descriptors with missing fingerprints in key origin information 2024-04-19 09:59:48 +02:00
Craig Raw
33f439f49a minor text changes 2024-04-19 09:43:18 +02:00
Craig Raw
d68ab40c94 add wallet import for samourai backup export 2024-04-18 16:04:06 +02:00
Craig Raw
31346e2afa add mix selected button to the postmix account in desktop and terminal 2024-04-18 13:22:50 +02:00
Craig Raw
c407a41475 add fine adjustment control for fee rate slider using mouse scroll 2024-04-18 12:12:40 +02:00
Craig Raw
8baa8e2e96 support changing the frame rate of animated qrs with mouse scroll on the qr image 2024-04-18 11:45:10 +02:00
Craig Raw
72adbe44a7 cleanup 2024-04-18 10:06:35 +02:00
Craig Raw
dd64c18c21 followup 2024-04-18 09:45:58 +02:00
Craig Raw
2354b061a9 add user write permissions to jvm legal files 2024-04-18 09:29:11 +02:00
Craig Raw
a5050117a3 handle null proxy configuration when fetching proxy 2024-04-17 14:11:41 +02:00
Craig Raw
f245b57022 add export search results as csv functionality 2024-04-17 14:01:55 +02:00
Craig Raw
d3752a856b make search wallet dialog non-modal, close any non-modal dialogs on application closing 2024-04-17 12:33:13 +02:00
Craig Raw
fe7dba6d83 support searching on multiple addresses, txids and utxos in a search phrase separated by spaces 2024-04-17 09:22:31 +02:00
Craig Raw
2d0a94d024 add copy context menu to date/address/output column in search wallet dialog 2024-04-16 14:34:27 +02:00
Craig Raw
41146310d6 allow label editing in the search wallet dialog 2024-04-16 13:26:58 +02:00
Craig Raw
a167f6aedb add show all wallets summary 2024-04-16 12:31:56 +02:00
Craig Raw
0fed7c45ee change transactions and utxos csv export to use utc timezone for dates 2024-04-16 10:35:42 +02:00
Craig Raw
5a0df265bc unminimize existing app window when second instance is launched 2024-04-15 12:40:17 +02:00
Craig Raw
646b8b0e65 show discover button when adding accounts on a bitcoin core connection, but warn user account discovery is not generally supported if no accounts are found 2024-04-15 09:55:24 +02:00
Craig Raw
c9b40b1973 fall back to coldcard singlesig import if multisig format import fails 2024-04-15 08:32:38 +02:00
Craig Raw
9ec5b6ce26 fix network enum error on startup in signet 2024-04-14 08:13:30 +02:00
Craig Raw
93893111c6 fix issue where gap limit was not increased when wallet could partially sign transaction 2024-04-12 09:12:33 +02:00
Craig Raw
3600d32ffd bump to v1.8.6 2024-04-11 13:48:12 +02:00
Craig Raw
1e0855c11d use hwi 3.0.0 built on ubuntu 20.04 amd64 2024-04-11 10:46:44 +02:00
Craig Raw
15cb028951 merge whirlpool 1.x client using decentralized soroban 2024-04-11 08:37:28 +02:00
Craig Raw
e178168bec fix npe on soroban counterparty timeout 2024-04-11 08:09:08 +02:00
Craig Raw
5696e00cb5 fix stonewallx2 transaction address selection 2024-04-10 12:42:11 +02:00
craigraw
594a873f20
Merge pull request #1369 from zeroleak/whirlpool-1.0.6
upgrade to whirlpool 1.0.6 to support simpler onion switching
2024-04-10 10:34:47 +02:00
zeroleak
da30d4223a upgrade whirlpool-client 1.0.6 2024-04-10 09:52:41 +02:00
Craig Raw
2441b4d7c3 handle coldcard singlesig file imports containing p2sh-p2wsh 2024-04-10 07:44:24 +02:00
Craig Raw
cc739a71e9 followup 2024-04-09 11:03:20 +02:00
Craig Raw
5f98eb9eb9 followup 2024-04-08 13:45:52 +02:00
Craig Raw
5aa25b98c3 supporting importing labels from electrum history csv using wallet labels import 2024-04-08 13:38:36 +02:00
Craig Raw
5058cd283d use hwi 3.0.0 built on ubuntu 20.04 aarch64 2024-04-08 11:36:27 +02:00
Craig Raw
af6171692b upgrade to hwi 3.0.0 2024-04-08 11:19:34 +02:00
Craig Raw
3c631fa653 add button to display seedqr on seed display dialog after warning 2024-04-05 13:50:05 +02:00
Craig Raw
10a796098b keep any existing seeds with matching fingerprints when changing a wallets output descriptor, rederiving the xpub if necessary 2024-04-05 12:11:46 +02:00
Craig Raw
8ac642b09c set default derivation for mnemonic and xprv imports to current keystore derivation 2024-04-04 14:05:41 +02:00
Craig Raw
33d9f260c4 close qr display dialog for current fresh address when it updates 2024-04-04 12:50:03 +02:00
770 changed files with 21767 additions and 14534 deletions

View File

@ -10,47 +10,64 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-2022, ubuntu-20.04, macos-12]
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-15-intel, macos-14]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up JDK 18.0.1
uses: actions/setup-java@v4
submodules: recursive
- name: Clear Java tool-cache for reproducibility
shell: bash
run: rm -rf "$RUNNER_TOOL_CACHE"/Java_*
- name: Set up JDK 25.0.2
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '18.0.1'
java-version: '25.0.2'
- name: Show Build Versions
run: ./gradlew -v
- name: Build with Gradle
run: ./gradlew jpackage
- name: Package zip distribution
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
- name: Codesign, package and notarize macOS distribution
if: ${{ runner.os == 'macOS' }}
uses: sparrowwallet/github-actions/codesign-macos@v1
with:
app-name: Sparrow
certificate: ${{ secrets.MACOS_CERTIFICATE }}
certificate-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
notarization-password: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
- name: Package Windows zip distribution
if: ${{ runner.os == 'Windows' }}
run: ./gradlew packageZipDistribution
- name: Package tar distribution
- name: Package Linux tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Upload Artifacts
uses: actions/upload-artifact@v4
- name: Repackage Linux deb distribution
if: ${{ runner.os == 'Linux' }}
run: ./repackage.sh
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: Sparrow Build - ${{ runner.os }}
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
!build/jpackage/Sparrow.app/
- name: Headless build with Gradle
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true clean jpackage
- name: Package headless tar distribution
- name: Package Linux headless tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
- name: Rename Headless Artifacts
- name: Repackage Linux headless deb distribution
if: ${{ runner.os == 'Linux' }}
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
run: ./repackage.sh
- name: Upload Headless Artifact
if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: Sparrow Build - ${{ runner.os }} Headless
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
path: |
build/jpackage/*
!build/jpackage/Sparrow/

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "drongo"]
path = drongo
url = ../../sparrowwallet/drongo.git
[submodule "lark"]
path = lark
url = ../../sparrowwallet/lark.git

3
.sdkmanrc Normal file
View File

@ -0,0 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=25.0.2-tem

View File

@ -16,14 +16,14 @@ or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 18 or higher to be installed.
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
In order to build, Sparrow requires Java 25 or higher to be installed.
The release binaries are built with [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
If you are using [SDKMAN](https://sdkman.io/), you can use `sdk env install` to ensure you have the correct version.
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
`sudo apt install -y rpm fakeroot binutils`
The Sparrow binaries can be built from source using
`./gradlew jpackage`
@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
`./sparrow`
Java 18 or higher must be installed.
Java 25 or higher must be installed.
## Configuration
@ -64,10 +64,12 @@ Usage: sparrow [options]
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
--network, -n
Network to use
Possible Values: [mainnet, testnet, regtest, signet]
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
```
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
Note that testnet currently refers to testnet3.
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
`export SPARROW_NETWORK=testnet`
@ -83,7 +85,7 @@ When not explicitly configured using the command line argument above, Sparrow st
| Linux | ~/.sparrow |
| Windows | %APPDATA%/Sparrow |
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
## Reporting Issues

View File

@ -1,59 +1,38 @@
import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'extra-java-module-info'
id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.0.1'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'org.beryx.jlink' version '3.2.1'
id 'org.gradlex.extra-java-module-info' version '1.13.1'
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.21.0'
}
def sparrowVersion = '1.8.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
def targetName = ""
def osArch = "x64"
def releaseArch = "x86_64"
if(System.getProperty("os.arch") == "aarch64") {
osArch = "aarch64"
releaseArch = "aarch64"
targetName = "-" + osArch
}
def headless = "true".equals(System.getProperty("java.awt.headless"))
def vTor = '4.7.13-4'
def vKmpTor = '1.4.3'
def kmpOs = osName
if(os.macOsX) {
kmpOs = "macos"
} else if(os.windows) {
kmpOs = "mingw"
}
def kmpArch = "x64"
if(System.getProperty("os.arch") == "aarch64") {
kmpArch = "arm64"
}
group "com.sparrowwallet"
version "${sparrowVersion}"
group = 'com.sparrowwallet'
version = '2.5.3'
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/' }
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
tasks.withType(AbstractArchiveTask).configureEach {
useFileSystemPermissions()
}
javafx {
sdk = "/home/scy/git/jfx-sandbox/build/sdk"
version = "26"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@ -64,25 +43,26 @@ java {
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo'))
implementation('com.google.guava:guava:33.0.0-jre')
implementation('com.google.code.gson:gson:2.9.1')
implementation(project(':lark'))
implementation('com.google.guava:guava:33.5.0-jre')
implementation('com.google.code.gson:gson:2.13.2')
implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:4.0.3') {
implementation('com.zaxxer:HikariCP:7.0.2') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-core:3.20.0') {
implementation('org.jdbi:jdbi3-core:3.51.0') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') {
implementation('com.google.zxing:javase:3.5.4') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('org.jcommander:jcommander:3.0')
implementation('com.github.arteam:simple-json-rpc-core:1.3')
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
@ -90,25 +70,19 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.7.3')
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
implementation('de.jangassen:nsmenufx:3.1.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}"
if(kmpOs == "linux" && kmpArch == "arm64") {
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}")
} else {
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}")
}
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}")
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) {
implementation('org.controlsfx:controlsfx:11.2.3' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@ -119,26 +93,25 @@ dependencies {
}
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation ('org.slf4j:slf4j-api:2.0.12')
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
implementation ('org.slf4j:slf4j-api:2.0.17')
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.5')
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('org.apache.commons:commons-compress:1.25.0')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30')
implementation('com.googlecode.lanterna:lanterna:3.1.1')
implementation('net.coobird:thumbnailator:0.4.18')
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
implementation('org.apache.commons:commons-lang3:3.20.0')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('com.github.librepdf:openpdf:1.3.43')
implementation('com.googlecode.lanterna:lanterna:3.1.5')
implementation('net.coobird:thumbnailator:0.4.21')
implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
implementation('io.github.doblon8:jzbar:0.4.0')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
@ -151,17 +124,9 @@ compileJava {
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/${osArch}/**"
}
}
}
test {
useJUnitPlatform()
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
}
application {
@ -169,6 +134,12 @@ application {
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
"--enable-native-access=com.sparrowwallet.drongo",
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--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",
@ -178,27 +149,20 @@ application {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=com.sparrowwallet.sparrow",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
}
if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Headless", "-Dprism.order=sw"]
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
}
}
@ -212,15 +176,49 @@ jlink {
requires 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
requires 'static jdk.jfr'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
}
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6',
'--no-header-files', '--no-man-pages', '--ignore-signing-information',
'--exclude-files', '**.png',
'--exclude-resources',
'glob:/com.sparrowwallet.merged.module/META-INF/*,' +
'glob:/javafx.graphics/*.dylib,' +
'glob:/javafx.graphics/*.so,' +
'glob:/javafx.graphics/*.dll,' +
'glob:/com.sparrowwallet.drongo/native/**,' +
'glob:/com.sparrowwallet.sparrow/native/**,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
'glob:/org.usb4java/org/usb4java/linux-*/**,' +
'glob:/org.usb4java/org/usb4java/win32-*/**,' +
'glob:/org.hid4java/darwin-*/**,' +
'glob:/org.hid4java/linux-*/**,' +
'glob:/org.hid4java/win32-*/**,' +
'glob:/openpnp.capture.java/darwin-*/**,' +
'glob:/openpnp.capture.java/linux-*/**,' +
'glob:/openpnp.capture.java/win32-*/**,' +
'glob:/io.github.doblon8.jzbar/native/**']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=com.sparrowwallet.merged.module",
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--enable-native-access=com.sparrowwallet.sparrow",
"--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",
@ -229,19 +227,12 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
@ -253,7 +244,10 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"]
if(os.windows) {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
@ -262,55 +256,125 @@ jlink {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Headless", "-Dprism.order=sw"]
jvmArgs += ["-Dglass.platform=Headless"]
}
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "${sparrowVersion}"
appVersion = "${version}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
installerType = "msi"
}
if(os.linux) {
if(headless) {
installerName = "sparrowserver"
installerOptions = ['--license-file', 'LICENSE']
} else {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
installerName = "sparrowwallet"
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
}
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
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/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
imageOptions += ['--icon', 'src/main/deploy/package/macos/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macos/']
installerType = "dmg"
}
}
if(os.linux) {
jpackageImage {
dependsOn('prepareModulesDir', 'copyUdevRules')
}
}
}
task removeGroupWritePermission(type: Exec) {
if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
if(!headless) {
tasks.jpackage.dependsOn('copyMimeInfo')
}
} else {
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
}
tasks.register('addUserWritePermission', Exec) {
if(os.windows) {
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
} else {
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
}
}
tasks.register('copyUdevRules', Copy) {
from('lark/src/main/resources/udev')
into(layout.buildDirectory.dir('image/conf/udev'))
include('*')
}
tasks.register('prepareResourceDir', Copy) {
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
into(layout.buildDirectory.dir('deploy/package'))
include('*')
eachFile { file ->
if(file.name.equals('control') || file.name.endsWith('.spec')) {
filter { line ->
if(line.contains('${size}')) {
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
}
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
}
}
}
}
tasks.register('copyMimeInfo', Copy) {
mustRunAfter tasks.jpackageImage
from('src/main/deploy/package/linux')
into(layout.buildDirectory.dir('jpackage/Sparrow/lib'))
include('sparrowwallet-Sparrow-MimeInfo.xml')
}
static def getDirectorySize(File directory) {
long size = 0
if(directory.isFile()) {
size = directory.length()
} else if(directory.isDirectory()) {
directory.eachFileRecurse { file ->
if(file.isFile()) {
size += file.length()
}
}
}
return Long.toString(size/1024 as long)
}
tasks.register('removeGroupWritePermission', Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
}
task packageZipDistribution(type: Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip"
tasks.register('packageZipDistribution', Zip) {
archiveFileName = "Sparrow-${version}.zip"
destinationDirectory = file("$buildDir/jpackage")
preserveFileTimestamps = os.macOsX
from("$buildDir/jpackage/") {
include "Sparrow/**"
include "Sparrow.app/**"
}
}
task packageTarDistribution(type: Tar) {
tasks.register('packageTarDistribution', Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP
from("$buildDir/jpackage/") {
@ -318,61 +382,80 @@ task packageTarDistribution(type: Tar) {
}
}
def jnaPlatform
if(os.macOsX) {
jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
} else if(os.windows) {
jnaPlatform = "win32-x86-64"
} else {
jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
}
def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
def nativeLibJars = [
'jna-' : "com/sun/jna/${jnaPlatform}/*",
'argon2-jvm-2' : "${jnaPlatform}/*",
'hid4java-' : "${jnaPlatform}/*",
'openpnp-capture-java': "${jnaPlatform}/*",
'jSerialComm-' : "${serialOs}/${serialArch}/*",
'usb4java-' : "org/usb4java/${jnaPlatform}/*",
'jzbar-' : "native/${osName}/${osArch}/*",
]
tasks.register('extractNativeLibraries') {
dependsOn 'jlink'
doLast {
def imageLib = file("$buildDir/image/lib")
// Project-owned natives
copy {
from "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}", "src/main/resources/native/${osName}/${osArch}"
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
// JavaFX natives
def javafxClassifier = ""
if(os.macOsX) {
javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
} else if(os.windows) {
javafxClassifier = "win"
} else {
javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
}
def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
if(javafxJar) {
copy {
from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
}
// Third-party natives
nativeLibJars.each { prefix, includePattern ->
def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
if(jar) {
copy {
from(zipTree(jar)) { include includePattern }
into imageLib
eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
includeEmptyDirs = false
}
}
}
}
}
extraJavaModuleInfo {
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
exports('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.core.async')
exports('com.fasterxml.jackson.core.base')
exports('com.fasterxml.jackson.core.exc')
exports('com.fasterxml.jackson.core.filter')
exports('com.fasterxml.jackson.core.format')
exports('com.fasterxml.jackson.core.io')
exports('com.fasterxml.jackson.core.json')
exports('com.fasterxml.jackson.core.json.async')
exports('com.fasterxml.jackson.core.sym')
exports('com.fasterxml.jackson.core.type')
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
requires('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.annotation')
}
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
requires('java.desktop')
requires('java.logging')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('java.sql')
requires('java.xml')
exports('com.fasterxml.jackson.databind')
exports('com.fasterxml.jackson.databind.annotation')
exports('com.fasterxml.jackson.databind.cfg')
exports('com.fasterxml.jackson.databind.deser')
exports('com.fasterxml.jackson.databind.deser.impl')
exports('com.fasterxml.jackson.databind.deser.std')
exports('com.fasterxml.jackson.databind.exc')
exports('com.fasterxml.jackson.databind.ext')
exports('com.fasterxml.jackson.databind.introspect')
exports('com.fasterxml.jackson.databind.json')
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
exports('com.fasterxml.jackson.databind.jsonschema')
exports('com.fasterxml.jackson.databind.jsontype')
exports('com.fasterxml.jackson.databind.jsontype.impl')
exports('com.fasterxml.jackson.databind.module')
exports('com.fasterxml.jackson.databind.node')
exports('com.fasterxml.jackson.databind.ser')
exports('com.fasterxml.jackson.databind.ser.impl')
exports('com.fasterxml.jackson.databind.ser.std')
exports('com.fasterxml.jackson.databind.type')
exports('com.fasterxml.jackson.databind.util')
uses('com.fasterxml.jackson.databind.Module')
}
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
@ -380,7 +463,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
@ -388,61 +471,26 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
exports('org.openpnp.capture')
exports('org.openpnp.capture.library')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
requires('com.sun.jna')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
exports('org.json.simple.parser')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
requires('com.github.benmanes.caffeine')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
@ -451,23 +499,23 @@ extraJavaModuleInfo {
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.undo')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
@ -476,187 +524,47 @@ extraJavaModuleInfo {
requires('javafx.graphics')
requires('javafx.controls')
}
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
exports('io.reactivex.rxjavafx.schedulers')
requires('io.reactivex.rxjava2')
requires('javafx.graphics')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
exports('org.flywaydb.core')
exports('org.flywaydb.core.api')
exports('org.flywaydb.core.api.exception')
exports('org.flywaydb.core.api.configuration')
uses('org.flywaydb.core.extensibility.Plugin')
requires('java.sql')
}
module('org.fxmisc.wellbehaved:wellbehavedfx', 'org.fxmisc.wellbehaved') {
requires('javafx.base')
requires('javafx.graphics')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
requires('java.desktop')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
exports('co.nstant.in.cbor')
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
exports('org.apache.commons.codec')
}
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
exports('ch.qos.logback.core')
}
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
exports('com.fasterxml.jackson.datatype.jsr310')
}
module('json-20240205.jar', 'org.json', '20240205') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
exports('com.auth0.jwt')
}
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
exports('org.apache.commons.text')
}
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
exports('net.coobird.thumbnailator')
requires('java.desktop')
}
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
exports('org.girod.javafx.svgimage')
requires('javafx.graphics')
requires('java.xml')
}
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
exports('io.matthewnelson.kmp.tor')
requires('kmp.tor.binary.extract.jvm')
requires('kmp.tor.manager.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
requires('java.management')
}
if(kmpOs == "linux" && kmpArch == "arm64") {
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
} else {
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
}
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
exports('io.matthewnelson.kmp.tor.binary.extract')
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
requires('kotlin.stdlib')
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
requires('kmp.tor.binary.geoip.jvm')
}
module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.manager')
exports('io.matthewnelson.kmp.tor.manager.util')
requires('kmp.tor.controller.common.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
requires('kotlinx.atomicfu')
requires('kmp.tor.controller.jvm')
requires('kmp.tor.common.jvm')
}
module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.manager.common')
exports('io.matthewnelson.kmp.tor.manager.common.event')
exports('io.matthewnelson.kmp.tor.manager.common.state')
requires('kmp.tor.controller.common.jvm')
requires('kmp.tor.common.jvm')
requires('kotlin.stdlib')
}
module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.controller.common.config')
exports('io.matthewnelson.kmp.tor.controller.common.file')
exports('io.matthewnelson.kmp.tor.controller.common.control')
exports('io.matthewnelson.kmp.tor.controller.common.control.usecase')
exports('io.matthewnelson.kmp.tor.controller.common.events')
exports('io.matthewnelson.kmp.tor.controller.common.exceptions')
requires('kmp.tor.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.atomicfu')
}
module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.common.address')
requires('parcelize.jvm')
requires('kotlin.stdlib')
}
module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
requires('kmp.tor.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlinx.coroutines.core')
requires('kotlin.stdlib')
requires('kotlinx.atomicfu')
requires('encoding.core.jvm')
requires('encoding.base16.jvm')
}
module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.ext.callback.common')
}
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
requires('kmp.tor.manager.jvm')
requires('kmp.tor.ext.callback.common.jvm')
requires('kmp.tor.ext.callback.manager.common.jvm')
requires('kmp.tor.ext.callback.controller.common.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
}
module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
requires('kmp.tor.ext.callback.controller.common.jvm')
}
module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") {
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
}
module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") {
exports('io.matthewnelson.kmp.tor.binary.geoip')
exports('kmptor')
}
module("base16-jvm-2.0.0.jar", 'encoding.base16.jvm', "2.0.0") {
exports('io.matthewnelson.encoding.base16')
requires('encoding.core.jvm')
requires('kotlin.stdlib')
}
module("base32-jvm-2.0.0.jar", 'encoding.base32.jvm', "2.0.0")
module("base64-jvm-2.0.0.jar", 'encoding.base64.jvm', "2.0.0")
module("core-jvm-2.0.0.jar", 'encoding.core.jvm', "2.0.0") {
exports('io.matthewnelson.encoding.core')
requires('kotlin.stdlib')
}
module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") {
exports('io.matthewnelson.component.parcelize')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
module('org.jcommander:jcommander', 'org.jcommander') {
exports('com.beust.jcommander')
}
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
module('com.sparrowwallet:hid4java', 'org.hid4java') {
requires('com.sun.jna')
exports('org.hid4java')
exports('org.hid4java.jna')
}
module('com.sparrowwallet:usb4java', 'org.usb4java') {
exports('org.usb4java')
}
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
exports('com.jcraft.jzlib')
}
}
kmpTorResourceFilterJar {
keepTorCompilation("current","current")
}

View File

@ -1,30 +0,0 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
implementation 'org.javamodularity:moduleplugin:1.8.14'
implementation 'org.ow2.asm:asm:9.6'
}
repositories {
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
register("org-openjfx-javafxplugin") {
id = "org-openjfx-javafxplugin"
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
}
}
}

View File

@ -1,54 +0,0 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}

View File

@ -1,52 +0,0 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}

View File

@ -1,176 +0,0 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
//ignore
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
if(manifest == null) {
manifest = new Manifest();
}
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
}
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
for (String usesName : moduleInfo.getUses()) {
moduleVisitor.visitUse(usesName.replace('.', '/'));
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}

View File

@ -1,62 +0,0 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
private List<String> uses = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public void uses(String uses) {
this.uses.add(uses);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
protected List<String> getUses() {
return uses;
}
}

View File

@ -1,114 +0,0 @@
/*
* Copyright (c) 2018, 2020, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
package org.openjfx.gradle;
import org.gradle.api.GradleException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public enum JavaFXModule {
BASE,
GRAPHICS(BASE),
CONTROLS(BASE, GRAPHICS),
FXML(BASE, GRAPHICS),
MEDIA(BASE, GRAPHICS),
SWING(BASE, GRAPHICS),
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
static final String PREFIX_MODULE = "javafx.";
private static final String PREFIX_ARTIFACT = "javafx-";
private List<JavaFXModule> dependentModules;
JavaFXModule(JavaFXModule...dependentModules) {
this.dependentModules = List.of(dependentModules);
}
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
return Stream.of(JavaFXModule.values())
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
.findFirst();
}
public String getModuleName() {
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
}
public String getModuleJarFileName() {
return getModuleName() + ".jar";
}
public String getArtifactName() {
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
}
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
return p.matcher(jarFileName).matches();
}
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
validateModules(moduleNames);
return moduleNames.stream()
.map(JavaFXModule::fromModuleName)
.flatMap(Optional::stream)
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
.collect(Collectors.toSet());
}
public static void validateModules(List<String> moduleNames) {
var invalidModules = moduleNames.stream()
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
.collect(Collectors.toList());
if (! invalidModules.isEmpty()) {
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
}
}
public List<JavaFXModule> getDependentModules() {
return dependentModules;
}
public List<JavaFXModule> getMavenDependencies() {
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
dependencies.add(0, this);
return dependencies;
}
}

View File

@ -1,164 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
package org.openjfx.gradle;
import org.gradle.api.Project;
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
public class JavaFXOptions {
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
private final Project project;
private final JavaFXPlatform platform;
private String version = "16";
private String sdk;
private String configuration = "implementation";
private String lastUpdatedConfiguration;
private List<String> modules = new ArrayList<>();
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
public JavaFXOptions(Project project) {
this.project = project;
this.platform = JavaFXPlatform.detect(project);
}
public JavaFXPlatform getPlatform() {
return platform;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
updateJavaFXDependencies();
}
/**
* If set, the JavaFX modules will be taken from this local
* repository, and not from Maven Central
* @param sdk, the path to the local JavaFX SDK folder
*/
public void setSdk(String sdk) {
this.sdk = sdk;
updateJavaFXDependencies();
}
public String getSdk() {
return sdk;
}
/** Set the configuration name for dependencies, e.g.
* 'implementation', 'compileOnly' etc.
* @param configuration The configuration name for dependencies
*/
public void setConfiguration(String configuration) {
this.configuration = configuration;
updateJavaFXDependencies();
}
public String getConfiguration() {
return configuration;
}
public List<String> getModules() {
return modules;
}
public void setModules(List<String> modules) {
this.modules = modules;
updateJavaFXDependencies();
}
public void modules(String...moduleNames) {
setModules(List.of(moduleNames));
}
private void updateJavaFXDependencies() {
clearJavaFXDependencies();
String configuration = getConfiguration();
JavaFXModule.getJavaFXModules(this.modules).stream()
.sorted()
.forEach(javaFXModule -> {
if (customSDKArtifactRepository != null) {
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
} else {
project.getDependencies().add(configuration,
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
getVersion(), getPlatform().getClassifier()));
}
});
lastUpdatedConfiguration = configuration;
}
private void clearJavaFXDependencies() {
if (customSDKArtifactRepository != null) {
project.getRepositories().remove(customSDKArtifactRepository);
customSDKArtifactRepository = null;
}
if (sdk != null && ! sdk.isEmpty()) {
Map<String, String> dirs = new HashMap<>();
dirs.put("name", "customSDKArtifactRepository");
if (sdk.endsWith(File.separator)) {
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
} else {
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
}
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
}
if (lastUpdatedConfiguration == null) {
return;
}
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
if (configuration != null) {
if (customSDKArtifactRepository != null) {
configuration.getDependencies()
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
}
configuration.getDependencies()
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
}
}
}

View File

@ -1,91 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetector;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import java.awt.*;
import java.util.Arrays;
import java.util.stream.Collectors;
public enum JavaFXPlatform {
LINUX("linux", "linux-x86_64"),
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
WINDOWS("win", "windows-x86_64"),
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
OSX("mac", "osx-x86_64"),
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
private final String classifier;
private final String osDetectorClassifier;
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
this.classifier = classifier;
this.osDetectorClassifier = osDetectorClassifier;
}
public String getClassifier() {
return classifier;
}
public static JavaFXPlatform detect(Project project) {
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
if("true".equals(System.getProperty("java.awt.headless"))) {
osClassifier += "-monocle";
}
for ( JavaFXPlatform platform: values()) {
if ( platform.osDetectorClassifier.equals(osClassifier)) {
return platform;
}
}
String supportedPlatforms = Arrays.stream(values())
.map(p->p.osDetectorClassifier)
.collect(Collectors.joining("', '", "'", "'"));
throw new GradleException(
String.format(
"Unsupported JavaFX platform found: '%s'! " +
"This plugin is designed to work on supported platforms only." +
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
);
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetectorPlugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().create("configJavafxRun", ExecTask.class, project);
}
}

View File

@ -1,124 +0,0 @@
/*
* Copyright (c) 2019, 2021, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
package org.openjfx.gradle.tasks;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
import org.openjfx.gradle.JavaFXModule;
import org.openjfx.gradle.JavaFXOptions;
import org.openjfx.gradle.JavaFXPlatform;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
private final Project project;
private JavaExec execTask;
@Inject
public ExecTask(Project project) {
this.project = project;
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
if (execTask != null) {
execTask.dependsOn(this);
} else {
throw new GradleException("Run task not found.");
}
});
}
@TaskAction
public void action() {
if (execTask != null) {
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
JavaFXModule.validateModules(javaFXOptions.getModules());
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
if (!definedJavaFXModuleNames.isEmpty()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
);
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
if (moduleOptions != null) {
LOGGER.info("Modular JavaFX application found");
// Remove empty JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
} else {
LOGGER.info("Non-modular JavaFX application found");
// Remove all JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars);
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
var jvmArgs = new ArrayList<String>();
jvmArgs.add("--add-modules");
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
List<String> execJvmArgs = execTask.getJvmArgs();
if (execJvmArgs != null) {
jvmArgs.addAll(execJvmArgs);
}
jvmArgs.addAll(javaFXModuleJvmArgs);
execTask.setJvmArgs(jvmArgs);
}
}
} else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
}
}
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
return jar.isFile() &&
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
javaFXModule.compareJarFileName(platform, jar.getName()) ||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
}
}

View File

@ -12,62 +12,36 @@ Work on resolving both of these issues is ongoing.
### Install Java
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
#### Java from Adoptium github repo
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_10.zip)
#### Java from Adoptium deb repo
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
- Install dependencies:
```sh
sudo apt-get install -y wget curl apt-transport-https gnupg
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
```shell
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
export JAVA_HOME=$PWD/jdk-25.0.2+10
export PATH=$JAVA_HOME/bin:$PATH
```
Download Adoptium public PGP key:
```sh
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
```
#### Java from SDKMAN
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
```
gpg --import --import-options show-only adoptium.asc
```
If key doesn't match, do not proceed.
Add Adoptium PGP key to a the keyring shared folder:
```sh
sudo cp adoptium.asc /usr/share/keyrings/
```
Add Adoptium debian repository:
```sh
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
```
Update cache, install the desired temurin version and configure java to be linked to this same version:
```
sudo apt update -y
sudo apt-get install -y temurin-18-jdk=18.0.1+10
sudo update-alternatives --config java
```
#### Java from SDK
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
An alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 18.0.1-tem
sdk install java 25.0.2-tem
```
### Other requirements
@ -82,7 +56,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="1.8.4"
GIT_TAG="2.5.2"
```
The project can then be initially cloned as follows:
@ -100,7 +74,7 @@ git checkout "${GIT_TAG}"
```
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
This is due to the Git submodules which need to be checked out to the commit state they had at the time of the release.
Only then your build will be comparable to the provided one in the release section of Github.
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:

2
drongo

@ -1 +1 @@
Subproject commit 9872c6b6ecfa6f5d14cd2edcb5605b76e9cb7396
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

15
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -112,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -170,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -203,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

25
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

1
lark Submodule

@ -0,0 +1 @@
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169

48
repackage.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash
set -e # Exit on any error
# Define paths
BUILD_DIR="build"
JPACKAGE_DIR="$BUILD_DIR/jpackage"
TEMP_DIR="$BUILD_DIR/repackage"
# Find the .deb file in build/jpackage (assuming there is only one)
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
# Check if a .deb file was found
if [ -z "$DEB_FILE" ]; then
echo "Error: No .deb file found in $JPACKAGE_DIR"
exit 1
fi
# Extract the filename from the path for later use
DEB_FILENAME=$(basename "$DEB_FILE")
echo "Found .deb file: $DEB_FILENAME"
# Create a temp directory inside build to avoid file conflicts
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
# Extract the .deb file contents
ar x "../../$DEB_FILE"
# Decompress zst files to tar
unzstd control.tar.zst
unzstd data.tar.zst
# Compress tar files to xz
xz -c control.tar > control.tar.xz
xz -c data.tar > data.tar.xz
# Remove the original .deb file
rm "../../$DEB_FILE"
# Create the new .deb file with xz compression in the original location
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
# Clean up temp files
cd ../..
rm -rf "$TEMP_DIR"
echo "Repackaging complete: $DEB_FILENAME"

View File

@ -1,3 +1,4 @@
rootProject.name = 'sparrow'
include 'drongo'
include 'lark'

View File

@ -1,2 +0,0 @@
mime-type=x-scheme-handler/auth47
description=Auth47 Authentication URI

View File

@ -1,2 +0,0 @@
mime-type=x-scheme-handler/bitcoin
description=Bitcoin Scheme URI

View File

@ -1,2 +0,0 @@
mime-type=x-scheme-handler/lightning
description=LNURL URI

View File

@ -0,0 +1,12 @@
Package: sparrowserver
Version: ${version}-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
Architecture: ${arch}
Conflicts: sparrow (<= 2.1.4)
Replaces: sparrow (<= 2.1.4)
Provides: sparrowserver
Description: Sparrow Server
Depends: libc6, zlib1g
Installed-Size: ${size}

View File

@ -0,0 +1,85 @@
Summary: Sparrow Server
Name: sparrowserver
Version: ${version}
Release: 1
License: ASL 2.0
Vendor: Unknown
%if "x" != "x"
URL: https://sparrowwallet.com
%endif
%if "x/opt" != "x"
Prefix: /opt
%endif
Provides: sparrowserver
Obsoletes: sparrow <= 2.1.4
%if "xutils" != "x"
Group: utils
%endif
Autoprov: 0
Autoreq: 0
#comment line below to enable effective jar compression
#it could easily get your package size from 40 to 15Mb but
#build time will substantially increase and it may require unpack200/system java to install
%define __jar_repack %{nil}
# on RHEL we got unwanted improved debugging enhancements
%define _build_id_links none
%define package_filelist %{_builddir}/%{name}.files
%define app_filelist %{_builddir}/%{name}.app.files
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
%description
Sparrow Server
%global __os_install_post %{nil}
%prep
%build
%install
rm -rf %{buildroot}
install -d -m 755 %{buildroot}/opt/sparrowserver
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
install -d -m 755 %{buildroot}/lib/systemd/system
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
fi
%if "x%{_rpmdir}/../../LICENSE" != "x"
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
%endif
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
%if "x%{_rpmdir}/../../LICENSE" != "x"
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
%endif
%files -f %{package_filelist}
%if "x%{_rpmdir}/../../LICENSE" != "x"
%license "%{license_install_file}"
%endif
%post
package_type=rpm
%pre
package_type=rpm
%preun
package_type=rpm
%clean

View File

@ -1,10 +1,11 @@
[Desktop Entry]
Name=Sparrow
Comment=Sparrow
Exec=/opt/sparrow/bin/Sparrow %U
Icon=/opt/sparrow/lib/Sparrow.png
Exec=/opt/sparrowwallet/bin/Sparrow %U
Icon=/opt/sparrowwallet/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Finance;Network;
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
StartupWMClass=Sparrow
SingleMainWindow=true

View File

@ -0,0 +1,12 @@
Package: sparrowwallet
Version: ${version}-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
Architecture: ${arch}
Provides: sparrowwallet
Conflicts: sparrow (<= 2.1.4)
Replaces: sparrow (<= 2.1.4)
Description: Sparrow Wallet
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
Installed-Size: ${size}

View File

@ -0,0 +1,49 @@
#!/bin/sh
# postinst script for sparrowwallet
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see https://www.debian.org/doc/debian-policy/ or
# the debian-policy package
package_type=deb
case "$1" in
configure)
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then
groupadd -r plugdev
fi
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/psbt">
<comment>Partially Signed Bitcoin Transaction</comment>
<glob pattern="*.psbt"/>
</mime-type>
<mime-type type="application/bitcoin-transaction">
<comment>Bitcoin Transaction</comment>
<glob pattern="*.txn"/>
</mime-type>
<mime-type type="application/pgp-signature">
<comment>ASCII Armored File</comment>
<glob pattern="*.asc"/>
</mime-type>
<mime-type type="x-scheme-handler/bitcoin">
<comment>Bitcoin Scheme URI</comment>
</mime-type>
<mime-type type="x-scheme-handler/auth47">
<comment>Auth47 Authentication URI</comment>
</mime-type>
<mime-type type="x-scheme-handler/lightning">
<comment>LNURL URI</comment>
</mime-type>
</mime-info>

View File

@ -0,0 +1,260 @@
Summary: Sparrow
Name: sparrowwallet
Version: ${version}
Release: 1
License: ASL 2.0
Vendor: Unknown
%if "x" != "x"
URL: https://sparrowwallet.com
%endif
%if "x/opt" != "x"
Prefix: /opt
%endif
Provides: sparrowwallet
Obsoletes: sparrow <= 2.1.4
%if "xutils" != "x"
Group: utils
%endif
Autoprov: 0
Autoreq: 0
%if "xxdg-utils" != "x" || "x" != "x"
Requires: xdg-utils
%endif
#comment line below to enable effective jar compression
#it could easily get your package size from 40 to 15Mb but
#build time will substantially increase and it may require unpack200/system java to install
%define __jar_repack %{nil}
# on RHEL we got unwanted improved debugging enhancements
%define _build_id_links none
%define package_filelist %{_builddir}/%{name}.files
%define app_filelist %{_builddir}/%{name}.app.files
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
%description
Sparrow Wallet
%global __os_install_post %{nil}
%prep
%build
%install
rm -rf %{buildroot}
install -d -m 755 %{buildroot}/opt/sparrowwallet
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
install -d -m 755 %{buildroot}/lib/systemd/system
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
fi
%if "x%{_rpmdir}/../../LICENSE" != "x"
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
%endif
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
%if "x%{_rpmdir}/../../LICENSE" != "x"
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
%endif
%files -f %{package_filelist}
%if "x%{_rpmdir}/../../LICENSE" != "x"
%license "%{license_install_file}"
%endif
%post
package_type=rpm
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then
groupadd -r plugdev
fi
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
%pre
package_type=rpm
file_belongs_to_single_package ()
{
if [ ! -e "$1" ]; then
false
elif [ "$package_type" = rpm ]; then
test `rpm -q --whatprovides "$1" | wc -l` = 1
elif [ "$package_type" = deb ]; then
test `dpkg -S "$1" | wc -l` = 1
else
exit 1
fi
}
do_if_file_belongs_to_single_package ()
{
local file="$1"
shift
if file_belongs_to_single_package "$file"; then
"$@"
fi
}
if [ "$1" -gt 1 ]; then
:;
fi
%preun
package_type=rpm
file_belongs_to_single_package ()
{
if [ ! -e "$1" ]; then
false
elif [ "$package_type" = rpm ]; then
test `rpm -q --whatprovides "$1" | wc -l` = 1
elif [ "$package_type" = deb ]; then
test `dpkg -S "$1" | wc -l` = 1
else
exit 1
fi
}
do_if_file_belongs_to_single_package ()
{
local file="$1"
shift
if file_belongs_to_single_package "$file"; then
"$@"
fi
}
#
# Remove $1 desktop file from the list of default handlers for $2 mime type
# in $3 file dumping output to stdout.
#
desktop_filter_out_default_mime_handler ()
{
local defaults_list="$3"
local desktop_file="$1"
local mime_type="$2"
awk -f- "$defaults_list" <<EOF
BEGIN {
mime_type="$mime_type"
mime_type_regexp="~" mime_type "="
desktop_file="$desktop_file"
}
\$0 ~ mime_type {
\$0 = substr(\$0, length(mime_type) + 2);
split(\$0, desktop_files, ";")
remaining_desktop_files
counter=0
for (idx in desktop_files) {
if (desktop_files[idx] != desktop_file) {
++counter;
}
}
if (counter) {
printf mime_type "="
for (idx in desktop_files) {
if (desktop_files[idx] != desktop_file) {
printf desktop_files[idx]
if (--counter) {
printf ";"
}
}
}
printf "\n"
}
next
}
{ print }
EOF
}
#
# Remove $2 desktop file from the list of default handlers for $@ mime types
# in $1 file.
# Result is saved in $1 file.
#
desktop_uninstall_default_mime_handler_0 ()
{
local defaults_list=$1
shift
[ -f "$defaults_list" ] || return 0
local desktop_file="$1"
shift
tmpfile1=$(mktemp)
tmpfile2=$(mktemp)
cat "$defaults_list" > "$tmpfile1"
local v
local update=
for mime in "$@"; do
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
v="$tmpfile2"
tmpfile2="$tmpfile1"
tmpfile1="$v"
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
update=yes
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
fi
done
if [ -n "$update" ]; then
cat "$tmpfile1" > "$defaults_list"
desktop_trace "$defaults_list" file updated
fi
rm -f "$tmpfile1" "$tmpfile2"
}
#
# Remove $1 desktop file from the list of default handlers for $@ mime types
# in all known system defaults lists.
#
desktop_uninstall_default_mime_handler ()
{
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
desktop_uninstall_default_mime_handler_0 "$f" "$@"
done
}
desktop_trace ()
{
echo "$@"
}
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
%clean

View File

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.8.5</string>
<string>2.5.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@ -33,8 +33,12 @@
<string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
<key>CFBundleURLTypes</key>
<array>
<dict>

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ 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.OsType;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode;
@ -12,6 +13,7 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.control.DialogImage;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.Auth47;
@ -24,8 +26,8 @@ import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.subjects.PublishSubject;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
@ -43,7 +45,6 @@ import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Dialog;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.text.Font;
import javafx.stage.Screen;
@ -67,8 +68,12 @@ import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppServices {
@ -88,18 +93,13 @@ public class AppServices {
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
private static AppServices INSTANCE;
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
private final SorobanServices sorobanServices = new SorobanServices();
private InteractionServices interactionServices;
private final InteractionServices interactionServices;
private static HttpClientService httpClientService;
@ -109,6 +109,8 @@ public class AppServices {
private TrayManager trayManager;
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
private static Image windowIcon;
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
@ -131,12 +133,18 @@ public class AppServices {
private static BlockHeader latestBlockHeader;
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
private static Map<Integer, Double> targetBlockFeeRates;
private static Double nextBlockMedianFeeRate;
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
private static Double serverMinimumRelayFeeRate;
private static CurrencyRate fiatCurrencyExchangeRate;
private static List<Device> devices;
@ -187,9 +195,13 @@ public class AppServices {
private AppServices(Application application, InteractionServices interactionServices) {
this.application = application;
this.interactionServices = interactionServices;
newBlockSubject.buffer(4, TimeUnit.SECONDS)
.filter(newBlockEvents -> !newBlockEvents.isEmpty())
.observeOn(JavaFxScheduler.platform())
.subscribe(this::fetchBlockSummaries, exception -> log.error("Error fetching block summaries", exception));
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
}
public void start() {
@ -202,6 +214,7 @@ public class AppServices {
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
if(config.getMode() == Mode.ONLINE) {
if(config.requiresInternalTor()) {
@ -268,7 +281,7 @@ public class AppServices {
}
if(Tor.getDefault() != null) {
Tor.getDefault().getTorManager().destroy(true, success -> {});
Tor.getDefault().close();
}
}
@ -298,12 +311,6 @@ public class AppServices {
if(event != null) {
EventManager.get().post(event);
}
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
}
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
@ -323,6 +330,9 @@ public class AppServices {
"\n\nChange the configured server certificate if you would like to proceed.");
} else {
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
if(crtFile == null) {
crtFile = Storage.getCaCertificateFile(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 be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
@ -359,15 +369,18 @@ public class AppServices {
onlineProperty.setValue(false);
onlineProperty.addListener(onlineServicesListener);
log.debug("Connection failed", failEvent.getSource().getException());
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
Config.get().changePublicServer();
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
boolean changed = changePublicServer();
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
if(!changed) {
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
}
} else {
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
}
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
return connectionService;
@ -387,7 +400,7 @@ public class AppServices {
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
//Delay startup on first run, Windows requires a longer delay
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
ratesService.setDelay(OsType.getCurrent() == OsType.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true);
@ -487,6 +500,26 @@ public class AppServices {
}
}
private void fetchFeeRates() {
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
feeRatesService = createFeeRatesService();
feeRatesService.start();
}
}
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
if(isConnected()) {
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
blockSummaryService.setOnSucceeded(_ -> {
EventManager.get().post(blockSummaryService.getValue());
});
blockSummaryService.setOnFailed(failedState -> {
log.error("Error fetching block summaries", failedState.getSource().getException());
});
blockSummaryService.start();
}
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
@ -502,7 +535,7 @@ public class AppServices {
public static Proxy getProxy(String proxyCircuitId) {
Config config = Config.get();
Proxy proxy = null;
if(config.isUseProxy()) {
if(config.isUseProxy() && config.getProxyServer() != null) {
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
@ -534,14 +567,6 @@ public class AppServices {
return INSTANCE;
}
public static WhirlpoolServices getWhirlpoolServices() {
return get().whirlpoolServices;
}
public static SorobanServices getSorobanServices() {
return get().sorobanServices;
}
public static InteractionServices getInteractionServices() {
return get().interactionServices;
}
@ -589,6 +614,34 @@ public class AppServices {
}
}
public static void runAfterDelay(long delay, Runnable runnable) {
if(delay <= 0) {
if(Platform.isFxApplicationThread()) {
runnable.run();
} else {
Platform.runLater(runnable);
}
} else {
ScheduledService<Void> delayService = new ScheduledService<>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() {
return null;
}
};
}
};
delayService.setOnSucceeded(_ -> {
delayService.cancel();
runnable.run();
});
delayService.setDelay(Duration.millis(delay));
delayService.start();
}
}
private static Image getWindowIcon() {
if(windowIcon == null) {
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
@ -607,7 +660,7 @@ public class AppServices {
}
private static double getReducedWindowHeight() {
return org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.OSX ? 802d : 768d; //Check for menu bar of ~34px
return OsType.getCurrent() != OsType.MACOS ? 802d : 768d; //Check for menu bar of ~34px
}
public Application getApplication() {
@ -692,6 +745,10 @@ public class AppServices {
return latestBlockHeader;
}
public static Map<Integer, BlockSummary> getBlockSummaries() {
return blockSummaries;
}
public static Double getDefaultFeeRate() {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
@ -703,6 +760,30 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
public static List<Double> getLongFeeRatesRange() {
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return LONG_FEE_RATES_RANGE;
} else {
List<Double> longFeeRatesRange = new ArrayList<>();
longFeeRatesRange.add(minimumRelayFeeRate);
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
return longFeeRatesRange;
}
}
public static List<Double> getFeeRatesRange() {
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
} else {
List<Double> longFeeRatesRange = getLongFeeRatesRange();
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
}
}
public static Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
}
public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
}
@ -737,10 +818,18 @@ public class AppServices {
});
}
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
}
public static Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
}
public static Double getServerMinimumRelayFeeRate() {
return serverMinimumRelayFeeRate;
}
public static CurrencyRate getFiatCurrencyExchangeRate() {
return fiatCurrencyExchangeRate;
}
@ -754,8 +843,8 @@ public class AppServices {
}
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null) {
throw new IllegalArgumentException("Not a payjoin URI");
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
throw new IllegalArgumentException("Not a valid payjoin URI");
}
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
@ -767,6 +856,10 @@ public class AppServices {
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.isValid()) {
ElectrumServer.releaseSilentPaymentSubscription(wallet.getSilentPaymentScanAddress());
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
AppServices.clearTransactionHistoryCache(childWallet);
@ -778,6 +871,22 @@ public class AppServices {
return Storage.isWalletFile(file);
}
public boolean changePublicServer() {
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
}
private boolean changePublicServer(List<PolicyType> policyTypes) {
Config config = Config.get();
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
if(!otherServers.isEmpty()) {
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
return true;
}
return false;
}
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
@ -806,8 +915,13 @@ public class AppServices {
Stage stage = (Stage)window;
stage.getIcons().add(getWindowIcon());
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
if(stage.getScene() != null) {
if(Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
}
if(Config.get().isChunkAddresses()) {
stage.getScene().getRoot().getStyleClass().add("chunk-addresses");
}
}
}
@ -923,6 +1037,7 @@ public class AppServices {
}
if(openWindow instanceof Stage) {
((Stage)openWindow).setIconified(false);
((Stage)openWindow).setAlwaysOnTop(true);
((Stage)openWindow).setAlwaysOnTop(false);
}
@ -987,7 +1102,7 @@ public class AppServices {
try {
Auth47 auth47 = new Auth47(uri);
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
if(wallet != null) {
try {
@ -1007,8 +1122,8 @@ public class AppServices {
private static void openLnurlAuthUri(URI uri) {
try {
LnurlAuth lnurlAuth = new LnurlAuth(uri);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
if(wallet != null) {
if(wallet.isEncrypted()) {
@ -1081,8 +1196,7 @@ public class AppServices {
walletChoiceDialog.initOwner(getActiveWindow());
walletChoiceDialog.setTitle("Choose Wallet");
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
Image image = new Image("/image/sparrow-small.png");
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
moveToActiveWindowScreen(walletChoiceDialog);
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
@ -1094,17 +1208,98 @@ public class AppServices {
return wallet;
}
public static boolean disallowAnyInvalidDerivationPaths(Wallet wallet) {
Optional<ScriptType> optInvalidScriptType = wallet.getKeystores().stream()
.filter(keystore -> keystore.getKeyDerivation() != null)
.map(keystore -> wallet.getOtherScriptTypeMatchingDerivation(keystore.getKeyDerivation().getDerivationPath()))
.filter(Optional::isPresent).map(Optional::get).findFirst();
if(optInvalidScriptType.isPresent()) {
ScriptType invalidScriptType = optInvalidScriptType.get();
boolean includePolicyType = !wallet.getScriptType().getAllowedPolicyTypes().getFirst().equals(invalidScriptType.getAllowedPolicyTypes().getFirst());
Optional<ButtonType> optType = AppServices.showWarningDialog("Invalid derivation path", "This wallet is using the derivation path for " +
invalidScriptType.getDescription(includePolicyType) + ", instead of the derivation path for its defined script type of " + wallet.getScriptType().getDescription(includePolicyType) +
". \n\nDisable derivation path validation to import this wallet?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent()) {
if(optType.get() == ButtonType.YES) {
Config.get().setValidateDerivationPaths(false);
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(true));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(true));
} else {
return true;
}
}
}
return false;
}
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
public static boolean isWhirlpoolCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().get(0).hasSeed()
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
&& wallet.getStandardAccountType() != null
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
}
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
}
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
List<Wallet> childWallets = new ArrayList<>();
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
childWallets.add(childWallet);
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
}
}
return childWallets;
}
public static Font getMonospaceFont() {
return Font.font("Roboto Mono", 13);
return Font.font("Fragment Mono Regular", 13);
}
public static boolean isOnWayland() {
if(OsType.getCurrent() != OsType.UNIX) {
return false;
}
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
return waylandDisplay != null && !waylandDisplay.isEmpty();
}
@Subscribe
public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
}
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer();
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
fetchFeeRates();
}
if(!blockSummaries.containsKey(currentBlockHeight)) {
fetchBlockSummaries(Collections.emptyList());
}
}
@Subscribe
@ -1119,11 +1314,22 @@ public class AppServices {
latestBlockHeader = event.getBlockHeader();
String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status));
newBlockSubject.onNext(event);
}
@Subscribe
public void blockSummary(BlockSummaryEvent event) {
blockSummaries.putAll(event.getBlockSummaryMap());
if(AppServices.currentBlockHeight != null) {
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
}
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates();
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
@ -1136,10 +1342,8 @@ public class AppServices {
@Subscribe
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
//Perform once-off fee rates retrieval to immediately change displayed rates
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
feeRatesService = createFeeRatesService();
feeRatesService.start();
}
fetchFeeRates();
fetchBlockSummaries(Collections.emptyList());
}
@Subscribe
@ -1280,10 +1484,28 @@ public class AppServices {
@Subscribe
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
String currentName = Config.get().getServerDisplayName();
onlineProperty.set(false);
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
Config.get().changePublicServer();
boolean changed = changePublicServer();
if(changed) {
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
} else {
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
}
onlineProperty.set(true);
}
}
@Subscribe
public void silentPaymentsUnsubscribe(SilentPaymentsUnsubscribeEvent event) {
if(isConnected()) {
ElectrumServer.SilentPaymentsUnsubscribeService unsubscribeService = new ElectrumServer.SilentPaymentsUnsubscribeService(event.getScanAddress());
unsubscribeService.setOnFailed(workerStateEvent -> {
log.warn("Failed to unsubscribe silent payments for " + event.getScanAddress().getAddress(), workerStateEvent.getSource().getException());
});
unsubscribeService.start();
}
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.wallet.Keystore;
@ -54,7 +55,7 @@ public abstract class BaseController {
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);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? 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();
@ -72,10 +73,13 @@ public abstract class BaseController {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append("/");
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
builder.append("]");
builder.append(keystore.getExtendedPublicKey().toString());
if(keystore.getExtendedPublicKey() != null) {
builder.append(keystore.getExtendedPublicKey().toString());
} else if(keystore.getSilentPaymentScanAddress() != null) {
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
}
return builder.toString();
}

View File

@ -0,0 +1,76 @@
package com.sparrowwallet.sparrow;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;
public class BlockSummary implements Comparable<BlockSummary> {
private final Integer height;
private final Date timestamp;
private final Double medianFee;
private final Integer transactionCount;
private final Integer weight;
public BlockSummary(Integer height, Date timestamp) {
this(height, timestamp, null, null, null);
}
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
this.height = height;
this.timestamp = timestamp;
this.medianFee = medianFee;
this.transactionCount = transactionCount;
this.weight = weight;
}
public Integer getHeight() {
return height;
}
public Date getTimestamp() {
return timestamp;
}
public Optional<Double> getMedianFee() {
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
}
public Optional<Integer> getTransactionCount() {
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
}
public Optional<Integer> getWeight() {
return weight == null ? Optional.empty() : Optional.of(weight);
}
private static long calculateElapsedSeconds(long timestampUtc) {
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
Instant nowInstant = Instant.now();
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
}
public String getElapsed() {
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
if(elapsed < 0) {
return "now";
} else if(elapsed < 60) {
return elapsed + "s";
} else if(elapsed < 3600) {
return elapsed / 60 + "m";
} else if(elapsed < 86400) {
return elapsed / 3600 + "h";
} else {
return elapsed / 86400 + "d";
}
}
public String toString() {
return getElapsed() + ":" + getMedianFee();
}
@Override
public int compareTo(BlockSummary o) {
return o.height - height;
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
import com.sparrowwallet.sparrow.control.TextUtils;
@ -48,7 +49,7 @@ public class DefaultInteractionServices implements InteractionServices {
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}

View File

@ -8,13 +8,13 @@ public enum Interface {
public static Interface get() {
if(currentInterface == null) {
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
boolean glassHeadless = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
if(headless || glassHeadless) {
if(headless || headlessPlatform) {
currentInterface = TERMINAL;
if(headless && !glassHeadless) {
throw new UnsupportedOperationException("Headless environment detected but Headless platform not found");
if(headless && !headlessPlatform) {
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
}
} else {
currentInterface = DESKTOP;

View File

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.WalletIcon;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -9,13 +10,12 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.settings.SettingsGroup;
import com.sparrowwallet.sparrow.settings.SettingsDialog;
import javafx.application.Application;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.tools.Platform;
import org.slf4j.LoggerFactory;
import java.io.File;
@ -42,10 +42,7 @@ public class SparrowDesktop extends Application {
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);
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
initializeFonts();
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
AppServices.initialize(this);
@ -60,8 +57,8 @@ public class SparrowDesktop extends Application {
Config.get().setMode(mode);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
@ -75,10 +72,6 @@ public class SparrowDesktop extends Application {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
Config.get().setHdCapture(Boolean.TRUE);
}
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
@ -89,28 +82,42 @@ public class SparrowDesktop extends Application {
AppController appController = AppServices.newAppWindow(stage);
if(createNewWallet) {
appController.newWallet(null);
}
final boolean showNewWallet = createNewWallet;
//Delay opening new dialogs on Wayland
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
if(showNewWallet) {
appController.newWallet(null);
}
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
appController.openWalletFile(walletFile, false);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
appController.openWalletFile(walletFile, false);
}
}
}
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
AppServices.get().start();
});
}
private void initializeFonts() {
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
if(OsType.getCurrent() == OsType.MACOS) {
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
}
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
AppServices.get().start();
}
@Override

View File

@ -18,14 +18,24 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.8.5";
public static final String APP_VERSION = "2.5.3";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
private static Instance instance;
public static void main(String[] argv) {
if(System.getProperty(JPACKAGE_APP_PATH) != null) {
String libDir = System.getProperty("java.home") + File.separator + "lib";
System.setProperty("jna.boot.library.path", libDir);
System.setProperty("jna.library.path", libDir);
System.setProperty("jSerialComm.library.path", libDir);
System.setProperty("org.usb4java.LibraryName", "usb4java");
System.setProperty("java.library.path", libDir);
}
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);
@ -66,6 +76,11 @@ public class SparrowWallet {
Network.set(Network.TESTNET);
}
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
if(testnet4Flag.exists()) {
Network.set(Network.TESTNET4);
}
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);

View File

@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
welcomeController.initializeView();
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
dialogPane.setPrefHeight(540);
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);

View File

@ -1,21 +1,18 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
@ -46,12 +43,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
List<Integer> existingIndexes = new ArrayList<>();
Set<Integer> existingIndexes = new LinkedHashSet<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isNested()) {
existingIndexes.add(childWallet.getAccountIndex());
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
}
}
@ -62,16 +61,15 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
}
}
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
availableAccounts.add(WHIRLPOOL_PREMIX);
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
availableAccounts.add(WHIRLPOOL_POSTMIX);
}
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());

View File

@ -45,6 +45,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
setTooltip(tooltip);
getStyleClass().add("address-cell");
if(addressStatus.isDustAttack()) {
setGraphic(getDustAttackHyperlink(utxoEntry));

View File

@ -22,6 +22,7 @@ public class AddressLabel extends IdLabel {
public AddressLabel(String text) {
super(text);
setSkin(new AddressTextFieldSkin(this));
addressProperty().addListener((observable, oldValue, newValue) -> {
setAddressAsText(newValue);
contextMenu.copyHex.setText("Copy " + newValue.getOutputScriptDataType());

View File

@ -0,0 +1,148 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Network;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.skin.LabelSkin;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressLabelSkin extends LabelSkin {
public static final int CHUNK_SIZE = 4;
public static final Pattern CHUNK_PATTERN = Pattern.compile("(?<=\\G.{" + CHUNK_SIZE + "})");
private final TextFlow displayFlow;
private final ChangeListener<String> textListener;
private final ChangeListener<Font> fontListener;
public AddressLabelSkin(Label control) {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
getChildren().addFirst(displayFlow);
textListener = (_, _, newText) -> updateDisplay(newText);
fontListener = (_, _, _) -> updateDisplay(control.getText());
control.textProperty().addListener(textListener);
control.fontProperty().addListener(fontListener);
updateDisplay(control.getText());
control.setStyle("-fx-text-fill: transparent;");
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
getSkinnable().fontProperty().removeListener(fontListener);
super.dispose();
}
private void updateDisplay(String text) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
Text normalText = createText(text.substring(pos, span.start), false);
displayFlow.getChildren().add(normalText);
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
Text normalText = createText(text.substring(pos), false);
displayFlow.getChildren().add(normalText);
}
}
private void addChunkedAddress(String address) {
String[] chunks = CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
Text chunk = createText(chunks[i], i % 2 != 0);
displayFlow.getChildren().add(chunk);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.setFont(getSkinnable().getFont());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
Network network = Network.get();
return network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate) ||
candidate.startsWith(network.getBech32AddressHRP()) || candidate.startsWith(network.getSilentPaymentsAddressHrp());
}
@Override
protected void updateChildren() {
super.updateChildren();
if(displayFlow != null && !getChildren().contains(displayFlow)) {
getChildren().addFirst(displayFlow);
}
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
// Position TextFlow to align with the label's text area
Label label = getSkinnable();
Insets padding = label.getPadding();
Node graphic = label.getGraphic();
double graphicOffset = 0;
if(graphic != null && label.getContentDisplay() == ContentDisplay.LEFT) {
graphicOffset = graphic.getLayoutBounds().getWidth() + label.getGraphicTextGap();
}
displayFlow.resizeRelocate(
x + padding.getLeft() + graphicOffset,
y + padding.getTop(),
w - padding.getLeft() - padding.getRight() - graphicOffset,
h - padding.getTop() - padding.getBottom()
);
}
private record AddressSpan(int start, int end) {}
}

View File

@ -0,0 +1,274 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Bech32;
import impl.org.controlsfx.skin.CustomTextFieldSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.layout.Region;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.controlsfx.control.textfield.CustomTextField;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressTextFieldSkin extends CustomTextFieldSkin {
private static final boolean[] BASE58_OK = buildOkTable(new String(Base58.ALPHABET));
private static final boolean[] BECH32_DATA_OK = buildOkTable(Bech32.CHARSET);
private final TextFlow displayFlow;
private final Rectangle clip;
private final ChangeListener<String> textListener;
private final ChangeListener<Font> fontListener;
public AddressTextFieldSkin(TextField control) {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
clip = new Rectangle();
displayFlow.setClip(clip);
getChildren().addFirst(displayFlow);
textListener = (_, _, newText) -> updateDisplay(newText);
fontListener = (_, _, _) -> updateDisplay(control.getText());
control.textProperty().addListener(textListener);
control.fontProperty().addListener(fontListener);
updateDisplay(control.getText());
control.setStyle("-fx-text-fill: transparent;");
// Unbind caret color since it's normally bound to textFill
unbindCaretColor(getChildren());
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
getSkinnable().fontProperty().removeListener(fontListener);
super.dispose();
}
private void unbindCaretColor(javafx.collections.ObservableList<Node> children) {
for(Node node : children) {
if(node instanceof Path path && path.getStroke() != null) {
path.fillProperty().unbind();
path.strokeProperty().unbind();
path.getStyleClass().add("address-field-caret");
} else if(node instanceof javafx.scene.Parent parent) {
unbindCaretColor(parent.getChildrenUnmodifiable());
}
}
}
@Override
public ObjectProperty<Node> leftProperty() {
if(getSkinnable() instanceof CustomTextField customTextField) {
return customTextField.leftProperty();
}
return new SimpleObjectProperty<>();
}
@Override
public ObjectProperty<Node> rightProperty() {
if(getSkinnable() instanceof CustomTextField customTextField) {
return customTextField.rightProperty();
}
return new SimpleObjectProperty<>();
}
private void updateDisplay(String text) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
Text normalText = createText(text.substring(pos, span.start), false);
displayFlow.getChildren().add(normalText);
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
Text normalText = createText(text.substring(pos), false);
displayFlow.getChildren().add(normalText);
}
}
private void addChunkedAddress(String address) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
Text chunk = createText(chunks[i], i % 2 != 0);
displayFlow.getChildren().add(chunk);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.setFont(getSkinnable().getFont());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
if(candidate == null || candidate.isEmpty()) {
return false;
}
Network network = Network.get();
// Base58 (legacy) partial: must start with a legacy prefix and contain only base58 chars.
if(network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate)) {
return containsOnlyAscii(candidate, BASE58_OK);
}
String lower = candidate.toLowerCase(Locale.ROOT);
// Bech32 (segwit v0/v1) partial: starts with HRP, then optional '1', then bech32 data charset.
if(lower.startsWith(network.getBech32AddressHRP())) {
return isBech32LikePartial(lower);
}
// Silent payments partial (bech32-like): starts with its HRP, then optional '1', then bech32 data charset.
if(lower.startsWith(network.getSilentPaymentsAddressHrp())) {
return isBech32LikePartial(lower);
}
return false;
}
private static boolean isBech32LikePartial(String lower) {
int sep = lower.indexOf(Bech32.BECH32_SEPARATOR);
if(sep < 0) {
return containsOnlyHrpChars(lower);
}
String hrp = lower.substring(0, sep);
String dataPart = lower.substring(sep + 1);
if(hrp.isEmpty()) {
return false;
}
return containsOnlyHrpChars(hrp) && containsOnlyAscii(dataPart, BECH32_DATA_OK);
}
private static boolean containsOnlyHrpChars(String s) {
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
if(!ok) {
return false;
}
}
return true;
}
private static boolean[] buildOkTable(String allowed) {
boolean[] ok = new boolean[128];
for(int i = 0; i < allowed.length(); i++) {
char c = allowed.charAt(i);
if(c < ok.length) {
ok[c] = true;
} else {
throw new IllegalArgumentException("Non-ASCII allowed char: " + c);
}
}
return ok;
}
private static boolean containsOnlyAscii(String s, boolean[] ok) {
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c >= ok.length || !ok[c]) {
return false;
}
}
return true;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
Insets padding = getSkinnable().getPadding();
double leftWidth = 0;
double rightWidth = 0;
if(getSkinnable() instanceof CustomTextField customTextField) {
Node left = customTextField.getLeft();
Node right = customTextField.getRight();
if(left != null) {
leftWidth = left.getLayoutBounds().getWidth();
if(left instanceof Region leftRegion) {
leftWidth += leftRegion.getPadding().getLeft() + leftRegion.getPadding().getRight() + 1;
}
}
if(right != null) {
rightWidth = right.getLayoutBounds().getWidth();
if(right instanceof Region rightRegion) {
rightWidth += rightRegion.getPadding().getLeft() + rightRegion.getPadding().getRight();
}
}
}
double availableWidth = w - padding.getLeft() - padding.getRight() - leftWidth - rightWidth;
clip.setWidth(availableWidth);
clip.setHeight(h);
double topOffset = getSkinnable().getBaselineOffset() - displayFlow.getBaselineOffset();
displayFlow.resizeRelocate(
padding.getLeft() + leftWidth,
topOffset,
displayFlow.prefWidth(-1),
h - padding.getTop() - padding.getBottom()
);
}
private record AddressSpan(int start, int end) {}
}

View File

@ -0,0 +1,111 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import javafx.beans.value.ChangeListener;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddressTooltipSkin implements Skin<Tooltip> {
private final Tooltip tooltip;
private final TextFlow textFlow;
private final ChangeListener<String> textListener;
public AddressTooltipSkin(Tooltip tooltip) {
this.tooltip = tooltip;
textFlow = new TextFlow();
textFlow.getStyleClass().addAll(tooltip.getStyleClass());
textListener = (_, _, newText) -> updateDisplay(newText);
tooltip.textProperty().addListener(textListener);
updateDisplay(tooltip.getText());
}
@Override
public Tooltip getSkinnable() {
return tooltip;
}
@Override
public Node getNode() {
return textFlow;
}
@Override
public void dispose() {
tooltip.textProperty().removeListener(textListener);
}
private void updateDisplay(String text) {
textFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
List<AddressSpan> addresses = findAddresses(text);
int pos = 0;
for(AddressSpan span : addresses) {
if(span.start > pos) {
textFlow.getChildren().add(createText(text.substring(pos, span.start), false));
}
addChunkedAddress(text.substring(span.start, span.end));
pos = span.end;
}
if(pos < text.length()) {
textFlow.getChildren().add(createText(text.substring(pos), false));
}
}
private void addChunkedAddress(String address) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
for(int i = 0; i < chunks.length; i++) {
textFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
private List<AddressSpan> findAddresses(String text) {
List<AddressSpan> spans = new ArrayList<>();
Pattern wordBoundary = Pattern.compile("\\S+");
Matcher matcher = wordBoundary.matcher(text);
while(matcher.find()) {
String candidate = matcher.group();
if(isValidAddress(candidate)) {
spans.add(new AddressSpan(matcher.start(), matcher.end()));
}
}
return spans;
}
private boolean isValidAddress(String candidate) {
try {
Address.fromString(candidate);
return true;
} catch(InvalidAddressException e) {
return false;
}
}
private record AddressSpan(int start, int end) {}
}

View File

@ -30,7 +30,11 @@ public class AddressTreeTable extends CoinTreeTable {
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new EntryCell());
addressCol.setCellFactory(p -> {
EntryCell entryCell = new EntryCell();
entryCell.setSkin(new AddressTreeTableCellSkin<>(entryCell));
return entryCell;
});
addressCol.setSortable(false);
getColumns().add(addressCol);
@ -76,8 +80,9 @@ public class AddressTreeTable extends CoinTreeTable {
contextMenu.getItems().add(showCountItem);
getColumns().forEach(col -> col.setContextMenu(contextMenu));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
setupColumnWidths();
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(addressCol);

View File

@ -0,0 +1,128 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.skin.TreeTableCellSkin;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
private final TextFlow displayFlow;
private final ChangeListener<String> textListener;
private final Text ellipsisText;
private String currentDisplayedText;
public AddressTreeTableCellSkin(TreeTableCell<S, T> cell) {
super(cell);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
getChildren().add(displayFlow);
ellipsisText = new Text("...");
ellipsisText.fontProperty().bind(cell.fontProperty());
ellipsisText.getStyleClass().add("address-chunk");
textListener = (_, _, newText) -> updateDisplay(newText);
cell.textProperty().addListener(textListener);
updateDisplay(cell.getText());
cell.setStyle("-fx-text-fill: transparent;");
}
private void updateDisplay(String text) {
currentDisplayedText = text;
buildDisplay(text, false);
}
private void buildDisplay(String text, boolean truncated) {
displayFlow.getChildren().clear();
if(text == null || text.isEmpty()) {
return;
}
if(getSkinnable().getStyleClass().contains("address-cell")) {
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(text);
for(int i = 0; i < chunks.length; i++) {
displayFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
}
} else {
Text normalText = createText(text, false);
displayFlow.getChildren().add(normalText);
}
if(truncated) {
displayFlow.getChildren().add(ellipsisText);
}
}
private Text createText(String content, boolean alternate) {
Text text = new Text(content);
text.fontProperty().bind(getSkinnable().fontProperty());
text.getStyleClass().add("address-chunk");
if(alternate) {
text.getStyleClass().add("alternate");
}
return text;
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
TreeTableCell<S, T> cell = getSkinnable();
Insets padding = cell.getPadding();
double leftOffset = 0;
double topOffset = y + padding.getTop();
Text labeledText = (Text)getChildren().stream().filter(n -> n instanceof Text).findFirst().orElse(null);
if(labeledText != null) {
leftOffset = labeledText.getLayoutX();
topOffset = labeledText.getLayoutY() - labeledText.getBaselineOffset();
String fullText = cell.getText();
String displayedText = labeledText.getText();
if(fullText != null && displayedText != null && !fullText.equals(displayedText)) {
String ellipsis = cell.getEllipsisString();
if(displayedText.endsWith(ellipsis)) {
String truncatedText = displayedText.substring(0, displayedText.length() - ellipsis.length());
if(!truncatedText.equals(currentDisplayedText)) {
currentDisplayedText = truncatedText;
buildDisplay(truncatedText, true);
}
}
} else if(fullText != null && !fullText.equals(currentDisplayedText)) {
currentDisplayedText = fullText;
buildDisplay(fullText, false);
}
}
displayFlow.resizeRelocate(
leftOffset,
topOffset,
w - padding.getLeft() - padding.getRight(),
h - padding.getTop() - padding.getBottom()
);
}
@Override
protected void updateChildren() {
super.updateChildren();
if(displayFlow != null && !getChildren().contains(displayFlow)) {
getChildren().add(displayFlow);
}
}
@Override
public void dispose() {
getSkinnable().textProperty().removeListener(textListener);
super.dispose();
}
}

View File

@ -128,4 +128,15 @@ public class BalanceChart extends LineChart<Number, Number> {
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
public void refreshAxisLabels() {
NumberAxis yaxis = (NumberAxis)getYAxis();
// Force the axis to redraw by invalidating the upper and lower bounds
yaxis.setAutoRanging(false);
double lower = yaxis.getLowerBound();
double upper = yaxis.getUpperBound();
yaxis.setLowerBound(lower);
yaxis.setUpperBound(upper);
yaxis.setAutoRanging(true);
}
}

View File

@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import static com.sparrowwallet.sparrow.AppServices.*;
public class BitBoxPairingDialog extends Alert {
public BitBoxPairingDialog(String code) {
super(AlertType.INFORMATION);
initOwner(getActiveWindow());
setStageIcon(getDialogPane().getScene().getWindow());
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
setTitle("Confirm BitBox02 Pairing");
setHeaderText(getTitle());
VBox vBox = new VBox(20);
vBox.setAlignment(Pos.CENTER);
vBox.setPadding(new Insets(10, 20, 10, 20));
Label instructions = new Label("Confirm the following code is shown on BitBox02");
Label codeLabel = new Label(code);
codeLabel.getStyleClass().add("fixed-width");
vBox.getChildren().addAll(instructions, codeLabel);
getDialogPane().setContent(vBox);
moveToActiveWindowScreen(this);
getDialogPane().getButtonTypes().clear();
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
}
}

View File

@ -0,0 +1,372 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.BlockSummary;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.scene.Group;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration;
import org.girod.javafx.svgimage.SVGImage;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
public class BlockCube extends Group {
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
public static final double CUBE_SIZE = 60;
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
private final StringProperty elapsedProperty = new SimpleStringProperty("");
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
private Polygon front;
private Rectangle unusedArea;
private Rectangle usedArea;
private final Text heightText = new Text();
private final Text medianFeeText = new Text();
private final Text unitsText = new Text();
private final TextFlow medianFeeTextFlow = new TextFlow();
private final Text txCountText = new Text();
private final Text elapsedText = new Text();
private final Group feeRateIcon = new Group();
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
this.confirmedProperty.set(confirmed);
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
this.feeRatesSource.set(feeRatesSource);
this.weightProperty.addListener((_, _, _) -> {
if(front != null) {
updateFill();
}
});
this.medianFeeProperty.addListener((_, _, newValue) -> {
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
});
this.txCountProperty.addListener((_, _, newValue) -> {
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
});
this.timestampProperty.addListener((_, _, newValue) -> {
elapsedProperty.set(getElapsed(newValue.longValue()));
});
this.elapsedProperty.addListener((_, _, newValue) -> {
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
});
this.heightProperty.addListener((_, _, newValue) -> {
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
});
this.confirmedProperty.addListener((_, _, _) -> {
if(front != null) {
updateFill();
}
});
this.feeRatesSource.addListener((_, _, _) -> {
if(front != null) {
updateFill();
}
});
this.medianFeeText.textProperty().addListener((_, _, _) -> {
pulse();
});
if(weight != null) {
this.weightProperty.set(weight);
}
if(medianFee != null) {
this.medianFeeProperty.set(medianFee);
}
if(height != null) {
this.heightProperty.set(height);
}
if(txCount != null) {
this.txCountProperty.set(txCount);
}
if(timestamp != null) {
this.timestampProperty.set(timestamp);
}
drawCube();
}
private void drawCube() {
double depth = CUBE_SIZE * 0.2;
double perspective = CUBE_SIZE * 0.04;
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
front.getStyleClass().add("block-front");
front.setFill(null);
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
unusedArea.getStyleClass().add("block-unused");
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
usedArea.getStyleClass().add("block-used");
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
top.getStyleClass().add("block-top");
top.setStroke(null);
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
left.getStyleClass().add("block-left");
left.setStroke(null);
updateFill();
heightText.getStyleClass().add("block-height");
heightText.setFont(new Font(11));
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
heightText.setY(-24);
medianFeeText.getStyleClass().add("block-text");
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
unitsText.getStyleClass().add("block-text");
unitsText.setFont(new Font(10));
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
medianFeeTextFlow.setTranslateY(7);
txCountText.getStyleClass().add("block-text");
txCountText.setFont(new Font(10));
txCountText.setOpacity(0.7);
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
txCountText.setY(34);
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
feeRateIcon.setTranslateY(-36);
elapsedText.getStyleClass().add("block-text");
elapsedText.setFont(new Font(10));
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
elapsedText.setY(50);
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
}
private void updateFill() {
if(isConfirmed()) {
getStyleClass().removeAll("block-unconfirmed");
if(!getStyleClass().contains("block-confirmed")) {
getStyleClass().add("block-confirmed");
}
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
unusedArea.setHeight(startYAbsolute);
unusedArea.setStyle(null);
usedArea.setY(startYAbsolute);
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
usedArea.setVisible(true);
heightText.setVisible(true);
feeRateIcon.getChildren().clear();
} else {
getStyleClass().removeAll("block-confirmed");
if(!getStyleClass().contains("block-unconfirmed")) {
getStyleClass().add("block-unconfirmed");
}
usedArea.setVisible(false);
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
heightText.setVisible(false);
if(feeRatesSource.get() != null) {
SVGImage svgImage = feeRatesSource.get().getSVGImage();
if(svgImage != null) {
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
} else {
feeRateIcon.getChildren().clear();
}
} else {
feeRateIcon.getChildren().clear();
}
}
}
public void pulse() {
if(isConfirmed()) {
return;
}
if(unusedArea != null) {
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
}
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
);
timeline.setCycleCount(1);
timeline.play();
}
private static long calculateElapsedSeconds(long timestampUtc) {
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
Instant nowInstant = Instant.now();
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
}
public static String getElapsed(long timestampUtc) {
long elapsed = calculateElapsedSeconds(timestampUtc);
if(elapsed < 60) {
return "Just now";
} else if(elapsed < 3600) {
return Math.round(elapsed / 60f) + "m ago";
} else if(elapsed < 86400) {
return Math.round(elapsed / 3600f) + "h ago";
} else {
return Math.round(elapsed / 86400d) + "d ago";
}
}
private String getFeeRateStyleName() {
double rate = getMedianFee();
int[] feeRateInterval = getFeeRateInterval(rate);
if(feeRateInterval[1] == Integer.MAX_VALUE) {
return "VSIZE2000-2200_COLOR";
}
int[] nextRateInterval = getFeeRateInterval(rate * 2);
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
}
private int[] getFeeRateInterval(double medianFee) {
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
if(feeRate <= medianFee && nextFeeRate > medianFee) {
return new int[] { feeRate, nextFeeRate };
}
}
return new int[] { 1, 2 };
}
public int getWeight() {
return weightProperty.get();
}
public IntegerProperty weightProperty() {
return weightProperty;
}
public void setWeight(int weight) {
weightProperty.set(weight);
}
public double getMedianFee() {
return medianFeeProperty.get();
}
public DoubleProperty medianFee() {
return medianFeeProperty;
}
public void setMedianFee(double medianFee) {
medianFeeProperty.set(medianFee);
}
public int getHeight() {
return heightProperty.get();
}
public IntegerProperty heightProperty() {
return heightProperty;
}
public void setHeight(int height) {
heightProperty.set(height);
}
public int getTxCount() {
return txCountProperty.get();
}
public IntegerProperty txCountProperty() {
return txCountProperty;
}
public void setTxCount(int txCount) {
txCountProperty.set(txCount);
}
public long getTimestamp() {
return timestampProperty.get();
}
public LongProperty timestampProperty() {
return timestampProperty;
}
public void setTimestamp(long timestamp) {
timestampProperty.set(timestamp);
}
public String getElapsed() {
return elapsedProperty.get();
}
public StringProperty elapsedProperty() {
return elapsedProperty;
}
public void setElapsed(String elapsed) {
elapsedProperty.set(elapsed);
}
public boolean isConfirmed() {
return confirmedProperty.get();
}
public BooleanProperty confirmedProperty() {
return confirmedProperty;
}
public void setConfirmed(boolean confirmed) {
confirmedProperty.set(confirmed);
}
public FeeRatesSource getFeeRatesSource() {
return feeRatesSource.get();
}
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
return feeRatesSource;
}
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
this.feeRatesSource.set(feeRatesSource);
}
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
}
}

View File

@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@ -43,14 +45,24 @@ public class CardImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
private final KeystoreCardImport importer;
private final PolicyType policyType;
private List<ChildNumber> derivation;
protected Button importButton;
private final SimpleStringProperty pin = new SimpleStringProperty("");
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
this.importer = importer;
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
this.policyType = wallet.getPolicyType();
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
}
private static List<ChildNumber> getDefaultDerivation(Wallet wallet, KeyDerivation defaultDerivation) {
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
return defaultDerivation.getDerivation();
}
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
}
@Override
@ -102,7 +114,7 @@ public class CardImportPane extends TitledDescriptionPane {
return;
}
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
});
@ -343,12 +355,14 @@ public class CardImportPane extends TitledDescriptionPane {
public static class CardImportService extends Service<Keystore> {
private final KeystoreCardImport cardImport;
private final PolicyType policyType;
private final String pin;
private final List<ChildNumber> derivation;
private final StringProperty messageProperty;
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
this.cardImport = cardImport;
this.policyType = policyType;
this.pin = pin;
this.derivation = derivation;
this.messageProperty = messageProperty;
@ -359,7 +373,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() {
@Override
protected Keystore call() throws Exception {
return cardImport.getKeystore(pin, derivation, messageProperty);
return cardImport.getKeystore(policyType, pin, derivation, messageProperty);
}
};
}

View File

@ -0,0 +1,251 @@
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.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreCodexImport;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.List;
public class CodexKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
private final KeystoreCodexImport importer;
private final KeyDerivation defaultDerivation;
private SplitMenuButton importButton;
private Button enterCodexButton;
private Button calculateButton;
protected Label validLabel;
protected Label invalidLabel;
protected final SimpleStringProperty secretShareProperty = new SimpleStringProperty("");
public CodexKeystoreImportPane(Wallet wallet, KeystoreCodexImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Enter secret share", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet;
this.importer = importer;
this.defaultDerivation = defaultDerivation;
createImportButton();
buttonBox.getChildren().add(importButton);
}
@Override
protected Control createButton() {
enterCodexButton = new Button("Enter Secret Share");
enterCodexButton.managedProperty().bind(enterCodexButton.visibleProperty());
enterCodexButton.setOnAction(event -> {
enterCodex();
});
return enterCodexButton;
}
private void enterCodex() {
setDescription("Enter secret share");
showHideLink.setVisible(false);
setContent(getSecretShareEntry());
setExpanded(true);
}
private void importKeystore(List<ChildNumber> derivation) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch(ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
setDefaultButton(importButton);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(getDefaultDerivation());
});
String[] accounts = new String[]{"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
}
private void onInputChange(boolean empty, boolean validChecksum) {
if(!empty) {
try {
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
validChecksum = true;
} catch(ImportException e) {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
}
private Node getSecretShareEntry() {
VBox vBox = new VBox(20);
vBox.setPadding(new Insets(10, 30, 10, 30));
HBox shareEntry = new HBox(10);
shareEntry.setAlignment(Pos.CENTER_LEFT);
Label shareLabel = new Label("Secret:");
TextField shareField = new TextField();
HBox.setHgrow(shareField, Priority.ALWAYS);
shareField.setPromptText("ms...");
shareField.textProperty().addListener((observable, oldValue, newValue) -> {
secretShareProperty.set(newValue);
});
shareEntry.getChildren().addAll(shareLabel, shareField);
vBox.getChildren().add(shareEntry);
AnchorPane buttonPane = new AnchorPane();
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
secretShareProperty.addListener((ChangeListener<String>) (c, oldval, newval) -> {
boolean empty = secretShareProperty.isEmpty().get();
boolean validChecksum = false;
onInputChange(empty, validChecksum);
});
HBox rightBox = new HBox();
rightBox.setSpacing(10);
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided secret share"));
calculateButton.setOnAction(event -> {
setExpanded(true);
enterCodexButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(false);
setContent(getDerivationEntry(getDefaultDerivation()));
});
rightBox.getChildren().add(calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
Platform.runLater(shareField::requestFocus);
return vBox;
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKeystore(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}

View File

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import javafx.scene.chart.NumberAxis;
import javafx.util.StringConverter;
@ -18,6 +19,10 @@ final class CoinAxisFormatter extends StringConverter<Number> {
@Override
public String toString(Number object) {
if(Config.get().isHideAmounts()) {
return "";
}
Double value = bitcoinUnit.getValue(object.longValue());
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
}

View File

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.sparrow.UnitFormat;
@ -16,7 +17,6 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.Region;
import javafx.util.Duration;
import org.controlsfx.tools.Platform;
import java.text.DecimalFormat;
@ -32,7 +32,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
tooltip.setShowDelay(Duration.millis(500));
contextMenu = new CoinContextMenu();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}
@ -58,16 +58,22 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
} else {
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
}
setTooltip(tooltip);
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
@ -86,14 +92,16 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry) {
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
tooltip.hideConfirmations();
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
setContentDisplay(ContentDisplay.RIGHT);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
if(hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && !Config.get().isHideAmounts()) {
setText("-" + getText());
}
} else {
setGraphic(null);
@ -148,6 +156,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText();
}
public void hideConfirmations() {
showConfirmations = false;
isCoinbase = false;
confirmationsProperty.unbind();
setTooltipText();
}
private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
}

View File

@ -13,6 +13,8 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
public class CoinLabel extends Label {
public static final String HIDDEN_AMOUNT_TEXT = "\u2022\u2022\u2022\u2022\u2022";
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
@ -49,6 +51,13 @@ public class CoinLabel extends Label {
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);

View File

@ -5,9 +5,9 @@ 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.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
@ -51,8 +51,14 @@ public class CoinTextFormatter extends TextFormatter<String> {
commasRemoved = newText.length() - noFractionCommaText.length();
}
if(!coinValidation.matcher(noFractionCommaText).matches()) {
return null;
Matcher matcher = coinValidation.matcher(noFractionCommaText);
if(!matcher.matches()) {
matcher.reset();
if(matcher.find()) {
noFractionCommaText = matcher.group();
} else {
return null;
}
}
if(unitFormat.getGroupingSeparator().equals(change.getText())) {

View File

@ -1,36 +1,58 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.SortDirection;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletTableChangedEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
import com.sparrowwallet.sparrow.wallet.Entry;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class CoinTreeTable extends TreeTableView<Entry> {
private TableType tableType;
private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat;
private CurrencyRate currencyRate;
protected static final double STANDARD_WIDTH = 100.0;
private final PublishSubject<WalletTableChangedEvent> walletTableSubject = PublishSubject.create();
private final Observable<WalletTableChangedEvent> walletTableEvents = walletTableSubject.debounce(1, TimeUnit.SECONDS);
public TableType getTableType() {
return tableType;
}
public void setTableType(TableType tableType) {
this.tableType = tableType;
}
public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit;
@ -88,7 +110,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + "..."));
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
@ -104,7 +126,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
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()) {
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
@ -115,6 +137,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
wallet.setBirthDate(optDate.get());
wallet.setBirthHeight(null);
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
@ -130,17 +153,146 @@ public class CoinTreeTable extends TreeTableView<Entry> {
}
stackPane.getChildren().add(hyperlink);
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
Date prunedDate = getPrunedDate();
if(prunedDate != null) {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
prunedLabel.setTranslateY(30);
stackPane.getChildren().add(prunedLabel);
}
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) {
if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
TreeTableColumn<Entry, ?> column = getColumns().get(columnIndex);
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
private boolean isFullyScanned(Wallet wallet) {
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
}
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
Date prunedDate = getPrunedDate();
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
}
return false;
}
private static Date getPrunedDate() {
Cormorant cormorant = ElectrumServer.getCormorant();
if(cormorant == null) {
return null;
}
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
return null;
}
return bitcoindClient.getCachedPrunedDate();
}
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
WalletTable.Sort columnSort = getSavedColumnSort();
if(columnSort == null) {
columnSort = new WalletTable.Sort(defaultColumnIndex, getSortDirection(defaultSortType));
}
setSortColumn(columnSort);
getSortOrder().addListener((ListChangeListener<? super TreeTableColumn<Entry, ?>>) c -> {
if(c.next()) {
walletTableChanged();
}
});
for(TreeTableColumn<Entry, ?> column : getColumns()) {
column.sortTypeProperty().addListener((_, _, _) -> walletTableChanged());
}
}
protected void resetSortColumn() {
setSortColumn(getColumnSort());
}
protected void setSortColumn(WalletTable.Sort sort) {
if(sort.sortColumn() >= 0 && sort.sortColumn() < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
TreeTableColumn<Entry, ?> column = getColumns().get(sort.sortColumn());
column.setSortType(sort.sortDirection() == SortDirection.DESCENDING ? TreeTableColumn.SortType.DESCENDING : TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(column);
}
}
private WalletTable.Sort getColumnSort() {
if(getSortOrder().isEmpty() || !getColumns().contains(getSortOrder().getFirst())) {
return new WalletTable.Sort(tableType == TableType.UTXOS ? getColumns().size() - 1 : 0, SortDirection.DESCENDING);
}
return new WalletTable.Sort(getColumns().indexOf(getSortOrder().getFirst()), getSortDirection(getSortOrder().getFirst().getSortType()));
}
private SortDirection getSortDirection(TreeTableColumn.SortType sortType) {
return sortType == TreeTableColumn.SortType.ASCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
}
private WalletTable.Sort getSavedColumnSort() {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
Wallet wallet = getRoot().getValue().getWallet();
WalletTable walletTable = wallet.getWalletTable(tableType);
if(walletTable != null) {
return walletTable.getSort();
}
}
return null;
}
protected void setupColumnWidths() {
Double[] savedWidths = getSavedColumnWidths();
for(int i = 0; i < getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = getColumns().get(i);
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
}
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
//Ignore initial resizes during layout
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
EventManager.get().post(event);
//Reset pref widths here so window resizes don't cause reversion to previously set pref widths
Double[] widths = event.getWalletTable().getWidths();
for(int i = 0; i < getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = getColumns().get(i);
column.setPrefWidth(widths != null && getColumns().size() == widths.length ? widths[i] : STANDARD_WIDTH);
}
});
}
private void walletTableChanged() {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
WalletTable walletTable = new WalletTable(tableType, getColumnWidths(), getColumnSort());
walletTableSubject.onNext(new WalletTableChangedEvent(getRoot().getValue().getWallet(), walletTable));
}
}
private Double[] getColumnWidths() {
return getColumns().stream().map(TableColumnBase::getWidth).toArray(Double[]::new);
}
private Double[] getSavedColumnWidths() {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
Wallet wallet = getRoot().getValue().getWallet();
WalletTable walletTable = wallet.getWalletTable(tableType);
if(walletTable != null) {
return walletTable.getWidths();
}
}
return null;
}
}

View File

@ -6,10 +6,16 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.textfield.CustomTextField;
import java.util.List;
public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
@ -68,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(comboProperty);
}
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
return new CustomContextMenu(customItems);
}
public class CustomContextMenu extends ContextMenu {
public CustomContextMenu(List<MenuItem> customItems) {
super();
setFont(null);
MenuItem undo = new MenuItem("Undo");
undo.setOnAction(_ -> undo());
MenuItem redo = new MenuItem("Redo");
redo.setOnAction(_ -> redo());
MenuItem cut = new MenuItem("Cut");
cut.setOnAction(_ -> cut());
MenuItem copy = new MenuItem("Copy");
copy.setOnAction(_ -> copy());
MenuItem paste = new MenuItem("Paste");
paste.setOnAction(_ -> paste());
MenuItem delete = new MenuItem("Delete");
delete.setOnAction(_ -> deleteText(getSelection()));
MenuItem selectAll = new MenuItem("Select All");
selectAll.setOnAction(_ -> selectAll());
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
getItems().addAll(customItems);
setOnShowing(_ -> {
boolean hasSelection = getSelection().getLength() > 0;
boolean hasText = getText() != null && !getText().isEmpty();
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
undo.setDisable(!isUndoable());
redo.setDisable(!isRedoable());
cut.setDisable(!isEditable() || !hasSelection);
copy.setDisable(!hasSelection);
paste.setDisable(!isEditable() || !clipboardHasContent);
delete.setDisable(!hasSelection);
selectAll.setDisable(!hasText);
});
}
}
}

View File

@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
public class ConfirmationAlert extends Alert {
private final CheckBox dontAskAgain;
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
super(AlertType.CONFIRMATION, contentText, buttons);
initOwner(getActiveWindow());
setStageIcon(getDialogPane().getScene().getWindow());
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
setTitle(title);
setHeaderText(title);
VBox contentBox = new VBox(20);
contentBox.setPadding(new Insets(10, 20, 10, 20));
Label contentLabel = new Label(contentText);
contentLabel.setWrapText(true);
dontAskAgain = new CheckBox("Don't ask again");
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
getDialogPane().setContent(contentBox);
}
public boolean isDontAskAgain() {
return dontAskAgain.isSelected();
}
}

View File

@ -10,8 +10,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
@ -29,6 +28,10 @@ public class CopyableCoinLabel extends CopyableLabel {
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> {
if(!event.getButton().equals(MouseButton.PRIMARY)) {
return;
}
if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit();
}
@ -67,6 +70,13 @@ public class CopyableCoinLabel extends CopyableLabel {
}
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
setTooltip(tooltip);
setContextMenu(contextMenu);

View File

@ -10,6 +10,7 @@ import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
@ -52,6 +53,7 @@ public class CopyableTextField extends CustomTextField {
selectedTextProperty().removeListener(selectionListener);
}
});
setContextMenu(new ContextMenu());
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {

View File

@ -11,7 +11,7 @@ import java.util.Date;
public class DateAxisFormatter extends StringConverter<Number> {
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
private final DateFormat dateFormat;
private int oddCounter;

View File

@ -14,8 +14,7 @@ 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.policy.PolicyType.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
if(SINGLE_HD.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)) {
if(MULTI_HD.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
@ -52,6 +51,12 @@ public class DescriptorArea extends CodeArea {
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(SINGLE_SP.equals(policyType)) {
append("sp(", "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(")", "descriptor-text");
}
}
public Wallet getWallet() {

View File

@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.PdfUtils;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.Button;
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
super(ur);
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, QREncoding encoding) {
super(ur, bbqr, false, false, encoding);
DialogPane dialogPane = getDialogPane();
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
@ -19,8 +20,13 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
pdfButton.setGraphicTextGap(5);
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null);
event.consume();
});
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
if(buttonBar != null) {
buttonBar.setButtonOrder("E+L+B+C+O");
}
}
}

View File

@ -12,16 +12,15 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@ -54,7 +53,8 @@ public class DevicePane extends TitledDescriptionPane {
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final KeyDerivation defaultDerivation;
private final KeyDerivation requiredDerivation;
private final String message;
private final List<StandardAccount> availableAccounts;
private final Device device;
@ -77,13 +77,14 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = requiredDerivation;
this.defaultDerivation = defaultDerivation;
this.requiredDerivation = requiredDerivation;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -105,12 +106,13 @@ public class DevicePane extends TitledDescriptionPane {
}
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = wallet;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -132,12 +134,13 @@ public class DevicePane extends TitledDescriptionPane {
}
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
@ -154,13 +157,14 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
public DevicePane(Wallet wallet, String message, KeyDerivation requiredDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.defaultDerivation = requiredDerivation;
this.requiredDerivation = requiredDerivation;
this.message = message;
this.availableAccounts = null;
this.device = device;
@ -182,12 +186,13 @@ public class DevicePane extends TitledDescriptionPane {
}
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@ -205,12 +210,13 @@ public class DevicePane extends TitledDescriptionPane {
}
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = deviceOperation;
this.wallet = null;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.defaultDerivation = null;
this.requiredDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@ -289,27 +295,50 @@ public class DevicePane extends TitledDescriptionPane {
}
private void createImportButton() {
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton = requiredDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
importKeystore(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation());
});
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
if(wallet.getPolicyType() == null) {
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
for(PolicyAndScriptType type : types) {
MenuItem item = new MenuItem(type.getDescription());
final List<ChildNumber> derivation = type.scriptType().getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
wallet.setPolicyType(type.policyType());
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
List<ScriptType> scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType());
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
}
importMenuButton.getItems().add(new SeparatorMenuItem());
MenuItem discoverItem = new MenuItem("Discover Wallet...");
discoverItem.setDisable(!AppServices.isConnected());
discoverItem.setOnAction(_ -> discoverWallet());
importMenuButton.getItems().add(discoverItem);
} 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;
@ -366,7 +395,7 @@ public class DevicePane extends TitledDescriptionPane {
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
if(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint())) {
signMessageButton.setDisable(true);
}
}
@ -375,7 +404,6 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
@ -436,6 +464,14 @@ public class DevicePane extends TitledDescriptionPane {
getAddressButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
return defaultDerivation.getDerivation();
}
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@ -456,20 +492,26 @@ public class DevicePane extends TitledDescriptionPane {
});
vBox.getChildren().addAll(pinField, enterPinButton);
TilePane tilePane = new TilePane();
tilePane.setPrefColumns(3);
tilePane.setHgap(10);
tilePane.setVgap(10);
tilePane.setMaxWidth(150);
tilePane.setMaxHeight(120);
GridPane gridPane = new GridPane();
gridPane.setHgap(10);
gridPane.setVgap(10);
gridPane.setMaxWidth(150);
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
for(int i = 0; i < digits.length; i++) {
Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]);
tilePane.getChildren().add(pinButton);
GridPane.setRowIndex(pinButton, i / 3);
GridPane.setColumnIndex(pinButton, i % 3);
if((i / 3) == 3) {
GridPane.setHgrow(pinButton, Priority.ALWAYS);
GridPane.setColumnSpan(pinButton, 3);
pinButton.setMaxWidth(Double.MAX_VALUE);
}
gridPane.getChildren().add(pinButton);
pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData());
});
@ -477,7 +519,7 @@ public class DevicePane extends TitledDescriptionPane {
HBox contentBox = new HBox();
contentBox.setSpacing(50);
contentBox.getChildren().add(tilePane);
contentBox.getChildren().add(gridPane);
contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER);
@ -497,7 +539,7 @@ public class DevicePane extends TitledDescriptionPane {
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
sendPassphraseButton.setText("Send Passphrase");
sendPassphraseButton.getStyleClass().add("default-button");
setDefaultButton(sendPassphraseButton);
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
@ -689,7 +731,7 @@ public class DevicePane extends TitledDescriptionPane {
return;
}
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
@ -708,13 +750,21 @@ public class DevicePane extends TitledDescriptionPane {
}
}
importXpub(derivation);
importKey(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
} else {
importKey(derivation);
}
}
private void importKey(List<ChildNumber> derivation) {
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
importSpscan(derivation);
} else {
importXpub(derivation);
}
@ -725,7 +775,7 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
ExtendedKey xpub = getXpubService.getValue();
try {
Keystore keystore = new Keystore();
@ -733,7 +783,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
keystore.setExtendedPublicKey(xpub);
importKeystore(derivation, keystore);
} catch(Exception e) {
@ -749,14 +799,44 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start();
}
private void importSpscan(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
getSpscanService.setOnSucceeded(workerStateEvent -> {
SilentPaymentScanAddress spscan = getSpscanService.getValue();
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.setSilentPaymentScanAddress(spscan);
importKeystore(derivation, keystore);
} catch(Exception e) {
setError("Could not retrieve spscan", e.getMessage());
}
});
getSpscanService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
importButton.setDisable(false);
});
setDescription("Importing...");
showHideLink.setVisible(false);
getSpscanService.start();
}
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH);
PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD;
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
@ -778,10 +858,12 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
@ -820,10 +902,12 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
@ -833,11 +917,31 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressService.start();
}
private byte[] getDeviceRegistration() {
Optional<Keystore> optKeystore = wallet.getKeystores().stream()
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
}
private void updateDeviceRegistrations(Set<byte[]> newDeviceRegistrations) {
if(!newDeviceRegistrations.isEmpty()) {
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores();
if(!registrationKeystores.isEmpty()) {
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(wallet, registrationKeystores));
}
}
}
private List<Keystore> getDeviceRegistrationKeystores() {
return wallet.getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
}
private void signMessage() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), requiredDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
@ -848,7 +952,7 @@ public class DevicePane extends TitledDescriptionPane {
signButton.setDisable(false);
}
} else {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, requiredDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
@ -862,37 +966,141 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void discoverWallet() {
importButton.setDisable(true);
importButton.setMaxHeight(importButton.getHeight());
ProgressIndicator progressIndicator = new ProgressIndicator(0);
progressIndicator.getStyleClass().add("button-progress");
importButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber());
rangeInputDialog.setTitle("Choose number of accounts");
rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected.");
Optional<Integer> optRange = rangeInputDialog.showAndWait();
if(optRange.isEmpty()) {
return;
}
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
if(device.getModel() == WalletModel.BITBOX_02) {
scriptTypes.remove(ScriptType.P2PKH);
}
for(ScriptType scriptType : scriptTypes) {
for(StandardAccount discoveryAccount : discoveryAccounts) {
derivationPaths.put(new Hwi.WalletType(scriptType, discoveryAccount), KeyDerivation.writePath(scriptType.getDefaultDerivation(discoveryAccount.getAccountNumber())));
}
}
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
getXpubsService.setOnSucceeded(_ -> {
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
try {
Wallet wallet = new Wallet(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(entry.getKey().scriptType());
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(entry.getValue());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
wallets.add(wallet);
} else {
Wallet masterWallet = wallets.getLast();
wallet.setName(entry.getKey().standardAccount().getName());
wallet.setMasterWallet(masterWallet);
masterWallet.getChildWallets().add(wallet);
}
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
walletDiscoveryService.setOnSucceeded(_ -> {
importButton.setGraphic(null);
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
if(optWallets.isPresent()) {
List<Wallet> discoveredWallets = optWallets.get();
if(discoveredWallets.size() > 1) {
for(Wallet wallet : discoveredWallets) {
wallet.setName(wallet.getName() + " " + wallet.getScriptType().getDescription());
}
}
EventManager.get().post(new WalletImportEvent(discoveredWallets));
} else {
AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
setDefaultStatus();
importButton.setDisable(false);
}
});
walletDiscoveryService.setOnFailed(failedEvent -> {
log.error("Failed to discover wallets", failedEvent.getSource().getException());
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
importButton.setGraphic(null);
importButton.setDisable(false);
});
walletDiscoveryService.start();
});
getXpubsService.setOnFailed(_ -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
importButton.setGraphic(null);
importButton.setDisable(false);
});
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
showHideLink.setVisible(false);
getXpubsService.start();
}
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
discoverKeystoresButton.setDisable(true);
discoverKeystoresButton.setMaxHeight(discoverKeystoresButton.getHeight());
ProgressIndicator progressIndicator = new ProgressIndicator(0);
progressIndicator.getStyleClass().add("button-progress");
discoverKeystoresButton.setGraphic(progressIndicator);
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
Map<Hwi.WalletType, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
accountDerivationPaths.put(availableAccount, derivationPath);
accountDerivationPaths.put(new Hwi.WalletType(wallet.getScriptType(), availableAccount), derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey(), keystore);
keystore.setExtendedPublicKey(entry.getValue());
importedKeystores.put(entry.getKey().standardAccount(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
@ -906,15 +1114,18 @@ public class DevicePane extends TitledDescriptionPane {
accountDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
accountDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
setDescription("Discovering...");
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
showHideLink.setVisible(false);
getXpubsService.start();
}
@ -971,13 +1182,12 @@ public class DevicePane extends TitledDescriptionPane {
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
importButton.getStyleClass().add("default-button");
setDefaultButton(importButton);
}
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(!device.isCard());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
setContent(getDerivationEntry(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
@ -996,7 +1206,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
if(defaultDevice) {
getPrivateKeyButton.getStyleClass().add("default-button");
setDefaultButton(getPrivateKeyButton);
}
getPrivateKeyButton.setVisible(true);
showHideLink.setVisible(false);
@ -1011,7 +1221,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(device.isCard() || keyDerivation != null);
derivationField.setDisable(device.isCard() || requiredDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@ -1027,7 +1237,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importXpub(importDerivation);
importKey(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -1203,4 +1413,10 @@ public class DevicePane extends TitledDescriptionPane {
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -0,0 +1,86 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.NamedArg;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.StackPane;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.Locale;
public class DialogImage extends StackPane {
private static final Logger log = LoggerFactory.getLogger(DialogImage.class);
public static final int WIDTH = 50;
public static final int HEIGHT = 50;
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
public DialogImage() {
getStyleClass().add("dialog-image");
setPrefSize(WIDTH, HEIGHT);
this.typeProperty.addListener((observable, oldValue, type) -> {
refresh(type);
});
}
public DialogImage(@NamedArg("type") Type type) {
this();
this.typeProperty.set(type);
}
public void refresh() {
Type type = getType();
refresh(type);
}
protected void refresh(Type type) {
SVGImage svgImage;
if(Config.get().getTheme() == Theme.DARK) {
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + "-invert.svg");
} else {
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + ".svg");
}
if(svgImage != null) {
getChildren().clear();
getChildren().add(svgImage);
}
}
public Type getType() {
return typeProperty.get();
}
public ObjectProperty<Type> typeProperty() {
return typeProperty;
}
public void setType(Type type) {
this.typeProperty.set(type);
}
private SVGImage loadSVGImage(String imageName) {
try {
URL url = AppServices.class.getResource(imageName);
if(url != null) {
return SVGLoader.load(url);
}
} catch(Exception e) {
log.error("Could not find image " + imageName);
}
return null;
}
public enum Type {
SPARROW, SEED, PAYNYM, BORDERWALLETS, USERADD, WHIRLPOOL;
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.pgp.PGPKeySource;
import com.sparrowwallet.drongo.pgp.PGPUtils;
@ -24,7 +25,6 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
@ -56,13 +56,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "zip");
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
private static final String[] SPARROW_RELEASE_ALT_PREFIXES = { "sparrowwallet-", "sparrowwallet_", "sparrowserver-", "sparrowserver_" };
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
@ -70,6 +72,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final ObjectProperty<File> initial = new SimpleObjectProperty<>();
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
@ -81,7 +84,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static File lastFileParent;
public DownloadVerifierDialog(File initialSignatureFile) {
public DownloadVerifierDialog(File initialFile) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
@ -223,11 +226,17 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
});
release.addListener((observable, oldValue, releaseFile) -> {
if(releaseFile != null) {
initial.set(null);
}
verify();
});
if(initialSignatureFile != null) {
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
if(initialFile != null) {
javafx.application.Platform.runLater(() -> {
initial.set(initialFile);
signature.set(initialFile);
});
}
}
@ -292,7 +301,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
publicKeyDisabled.set(true);
}
if(manifest.get().equals(release.get())) {
if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.get())) {
manifestDisabled.set(true);
releaseHash.setText("No hash required, signature signs release file directly");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
@ -455,7 +464,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(providedName::startsWith)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) {
String version = matcher.group();
@ -482,6 +492,22 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
File initialFile = initial.get();
if(initialFile != null && initialFile.exists()) {
for(File file : manifestMap.keySet()) {
if(initialFile.getName().equals(file.getName())) {
return initialFile;
}
}
List<List<String>> allExtensionLists = List.of(MACOS_RELEASE_EXTENSIONS, WINDOWS_RELEASE_EXTENSIONS, LINUX_RELEASE_EXTENSIONS, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS);
for(List<String> extensions : allExtensionLists) {
if(extensions.stream().anyMatch(ext -> initialFile.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
return initialFile;
}
}
}
List<String> releaseExtensions = getReleaseFileExtensions();
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
@ -500,9 +526,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private List<String> getReleaseFileExtensions() {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
OsType osType = OsType.getCurrent();
switch(osType) {
case MACOS -> {
return MACOS_RELEASE_EXTENSIONS;
}
case WINDOWS -> {
@ -515,10 +541,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private String getReleaseFileExample(String version) {
Platform platform = Platform.getCurrent();
OsType osType = OsType.getCurrent();
String arch = System.getProperty("os.arch");
switch(platform) {
case OSX -> {
switch(osType) {
case MACOS -> {
return "Sparrow-" + version + "-" + arch;
}
case WINDOWS -> {
@ -565,7 +591,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
if((name.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(name::startsWith))
&& file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
return matcher.find();
}
@ -574,10 +601,18 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
return false;
}
public static boolean isSparrowManifest(File manifestFile) {
return manifestFile.getName().startsWith(SPARROW_RELEASE_PREFIX) && manifestFile.getName().endsWith(SPARROW_MANIFEST_SUFFIX);
}
public void setSignatureFile(File signatureFile) {
signature.set(signatureFile);
}
public void setInitialFile(File initialFile) {
initial.set(initialFile);
}
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
@ -598,15 +633,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
vBox.getChildren().addAll(headerLabel, descriptionLabel);
add(vBox, 0, 0);
StackPane graphicContainer = new StackPane();
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
graphicContainer.getStyleClass().add("graphic-container");
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);
graphicContainer.getChildren().add(imageView);
}
add(graphicContainer, 1, 0);
ColumnConstraints textColumn = new ColumnConstraints();

View File

@ -1,9 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -54,7 +58,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
super.updateItem(entry, empty);
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
if(this == lastCell && !getTableRow().isVisible()) {
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
return;
}
lastCell = this;
@ -65,8 +69,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null);
setGraphic(null);
} else {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(entry instanceof TransactionEntry transactionEntry) {
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@ -100,7 +103,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@ -120,21 +123,20 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
} else if(entry instanceof NodeEntry nodeEntry) {
Address address = nodeEntry.getAddress();
getStyleClass().add("address-cell");
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
if(!nodeEntry.getNode().getWallet().isBip47()) {
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@ -162,8 +164,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
@ -211,13 +212,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
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()))
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
@ -242,12 +244,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList());
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
if(changeTotal == 0) {
//Add change output length to vSize if change was not present on the original transaction
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(KeyPurpose.CHANGE).getOutputScript());
vSize += changeOutput.getLength();
}
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
@ -256,7 +259,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups);
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
//If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@ -297,9 +300,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)";
}
if(txOutput.getScript().getToAddress() != null) {
Address address = txOutput.getScript().getToAddress();
if(address != null) {
long value = txOutput.getValue();
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
}
return null;
@ -328,15 +335,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(cancelTransaction) {
Payment existing = payments.get(0);
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
existing.getLabel(), existing.getAmount(), true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), existing.getLabel(), existing.getAmount(), true);
payments.clear();
payments.add(payment);
opReturns.clear();
}
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
}
private static Double getMaxFeeRate() {
@ -363,10 +372,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript());
long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = freshAddress.getScriptType().getInputVbytes();
Address receiveAddress = transactionEntry.getWallet().getNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), receiveAddress.getOutputScript());
long dustThreshold = receiveAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = receiveAddress.getScriptType().getInputVbytes();
double vSize = inputSize + txOutput.getLength();
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
@ -390,19 +399,23 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = new Payment(freshAddress, label, inputTotal, true);
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
label, inputTotal, true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
}
private static boolean canRBF(BlockTransaction blockTransaction) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
}
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
PolicyType policyType = wallet.getPolicyType();
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@ -459,7 +472,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
Long vSizefromTip = transactionEntry.getVSizeFromTip();
if(feeRate != null && vSizefromTip != null) {
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE);
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
String amount = vSizefromTip + " vB";
if(vSizefromTip > 1000 * 1000) {
@ -475,7 +488,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
}
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
}
return tooltip;
@ -543,6 +556,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
@ -552,7 +566,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
});
getItems().add(viewTransaction);
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
@ -563,7 +577,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(increaseFee);
}
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> {
@ -606,7 +620,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
}
private static class TransactionContextMenu extends ContextMenu {
protected static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
@ -656,7 +670,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
@ -805,18 +819,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("unconfirmed-row");
cell.getStyleClass().remove("summary-row");
cell.getStyleClass().remove("address-cell");
boolean addressCell = cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming");
cell.getStyleClass().remove("negative-amount");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
cell.getStyleClass().remove("number-field");
if(entry != null) {
if(entry instanceof TransactionEntry) {
if(entry instanceof TransactionEntry transactionEntry) {
cell.getStyleClass().add("transaction-row");
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(cell instanceof ConfirmationsListener confirmationsListener) {
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
@ -825,25 +838,36 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
confirmationsListener.getConfirmationsProperty().unbind();
}
}
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0 && !cell.getStyleClass().contains("label-cell")) {
cell.getStyleClass().add("number-field");
}
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry) {
} else if(entry instanceof UtxoEntry utxoEntry) {
cell.getStyleClass().add("utxo-row");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
} else if(entry instanceof HashIndexEntry) {
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
cell.getStyleClass().add("number-field");
}
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
cell.getStyleClass().add("unconfirmed-row");
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
cell.getStyleClass().add("summary-row");
}
}
}
private boolean isTableSizeRecalculation() {
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
&& frame.getMethodName().equals("releaseCell")));
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.application.Platform;
@ -7,14 +8,19 @@ import javafx.scene.Node;
import javafx.scene.control.Slider;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*;
public class FeeRangeSlider extends Slider {
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
public FeeRangeSlider() {
super(0, FEE_RATES_RANGE.size() - 1, 0);
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
setMajorTickUnit(1);
setMinorTickCount(0);
setSnapToTicks(false);
@ -25,11 +31,11 @@ public class FeeRangeSlider extends Slider {
setLabelFormatter(new StringConverter<>() {
@Override
public String toString(Double object) {
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
if(isLongFeeRange() && feeRate >= 1000) {
return feeRate / 1000 + "k";
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
}
return Long.toString(feeRate);
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
}
@Override
@ -45,30 +51,94 @@ public class FeeRangeSlider extends Slider {
updateMaxFeeRange(newValue.doubleValue());
}
});
setOnScroll(event -> {
if(event.getDeltaY() != 0) {
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
}
setFeeRate(newFeeRate);
}
});
}
public double getFeeRate() {
return Math.pow(2.0, getValue());
return getFeeRate(AppServices.getMinimumRelayFeeRate());
}
public double getFeeRate(Double minRelayFeeRate) {
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return Math.pow(2.0, getValue());
}
if(getValue() < 1.0d) {
if(minRelayFeeRate == 0.0d) {
return getValue();
}
return Math.pow(minRelayFeeRate, 1.0d - getValue());
}
return Math.pow(2.0, getValue() - 1.0d);
}
public void setFeeRate(double feeRate) {
double value = Math.log(feeRate) / Math.log(2);
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
}
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
double value = getValue(feeRate, minRelayFeeRate);
updateMaxFeeRange(value);
setValue(value);
}
private double getValue(double feeRate, Double minRelayFeeRate) {
double value;
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
value = Math.log(feeRate) / Math.log(2);
} else {
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
if(minRelayFeeRate == 0.0d) {
return feeRate;
}
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
} else {
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
}
}
return value;
}
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
}
setMinorTickCount(1);
setMinorTickCount(0);
}
private void updateMaxFeeRange(double value) {
if(value >= getMax() && !isLongFeeRange()) {
setMax(LONG_FEE_RATES_RANGE.size() - 1);
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
setMin(1.0d);
}
setMax(AppServices.getLongFeeRatesRange().size() - 1);
updateTrackHighlight();
} else if(value == getMin() && isLongFeeRange()) {
setMax(FEE_RATES_RANGE.size() - 1);
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
setMin(0.0d);
}
setMax(AppServices.getFeeRatesRange().size() - 1);
updateTrackHighlight();
}
}
private boolean isLongFeeRange() {
return getMax() > FEE_RATES_RANGE.size() - 1;
public boolean isLongFeeRange() {
return getMax() > AppServices.getFeeRatesRange().size() - 1;
}
public void updateTrackHighlight() {
@ -123,9 +193,9 @@ public class FeeRangeSlider extends Slider {
}
private int getPercentageOfFeeRange(Double feeRate) {
double index = Math.log(feeRate) / Math.log(2);
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
if(isLongFeeRange()) {
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
}
return (int)Math.round(index * 10.0);
}

View File

@ -1,8 +1,10 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
@ -10,7 +12,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.controlsfx.tools.Platform;
import java.math.BigDecimal;
import java.util.Currency;
@ -24,7 +25,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
tooltip = new Tooltip();
contextMenu = new FiatContextMenu();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}
@ -47,20 +48,27 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
if(currencyRate != null && currencyRate.isAvailable()) {
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setGraphic(null);
setTooltip(null);
setContextMenu(null);
} else {
Currency currency = currencyRate.getCurrency();
double btcRate = currencyRate.getBtcRate();
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
setText(label);
setGraphic(null);
setTooltip(tooltip);
setContextMenu(contextMenu);
}
} else {
setText(null);
setGraphic(null);

View File

@ -90,6 +90,13 @@ public class FiatLabel extends CopyableLabel {
private void setValueAsText(long balance, UnitFormat unitFormat) {
if(getCurrency() != null && getBtcRate() > 0.0) {
if(Config.get().isHideAmounts()) {
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
setTooltip(null);
setContextMenu(null);
return;
}
BigDecimal satsBalance = BigDecimal.valueOf(balance);
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));

View File

@ -1,11 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport;
@ -24,9 +26,7 @@ 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;
@ -45,8 +45,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private final boolean fileFormatAvailable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl);
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, walletModel);
this.importer = importer;
this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable;
@ -104,7 +104,7 @@ 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", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json"),
new FileChooser.ExtensionFilter("TXT", "*.txt")
);
@ -240,6 +240,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
javafx.application.Platform.runLater(passwordField::requestFocus);
return contentBox;
}
}

View File

@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
private final boolean file;
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
this.keystore = keystore;
this.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable();
@ -157,7 +157,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
if(exporter instanceof Bip129) {
UR ur = UR.fromBytes(baos.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, QREncoding.UR);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}

View File

@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
@ -25,11 +25,11 @@ public class FileKeystoreImportPane extends FileImportPane {
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = getScannedKeystore(wallet.getScriptType());
if(keystore == null) {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
keystore = importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType(), inputStream, password);
}
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + KeyDerivation.writePath(keystore.getKeyDerivation().getDerivation()) + ".");
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}

View File

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryItem;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final boolean file;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
this.wallet = wallet;
this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
@ -168,14 +168,21 @@ public class FileWalletExportPane extends TitledDescriptionPane {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Bip129) {
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.UR);
} else if(exporter instanceof Descriptor) {
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.BBQR);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}

View File

@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
private final WalletImport importer;
public FileWalletImportPane(WalletImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
this.importer = importer;
}

View File

@ -12,6 +12,7 @@ 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.application.Platform;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -29,8 +30,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
@ -38,28 +39,38 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private final KeystoreFileImport importer;
private String fileName;
private byte[] fileBytes;
private String password;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.importer = importer;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
this.password = password;
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD));
List<PolicyAndScriptType> walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList();
types.retainAll(walletTypes);
if(types.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
if(types.size() == 1) {
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new);
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
EventManager.get().post(new WalletImportEvent(wallet));
return;
}
} else {
try {
@ -69,58 +80,61 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
}
}
setContent(getScriptTypeEntry(scriptTypes));
setContent(getScriptTypeEntry(types));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
private void importWallet(PolicyAndScriptType type) throws ImportException {
PolicyType policyType = type.policyType();
ScriptType scriptType = type.scriptType();
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
Label label = new Label("Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
comboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
}
@Override
public ScriptType fromString(String string) {
public PolicyAndScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
comboBox.setMaxWidth(220);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -130,7 +144,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(scriptTypeComboBox.getValue());
importWallet(comboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
@ -151,6 +165,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
Platform.runLater(comboBox::requestFocus);
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -1,16 +1,20 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.Persistable;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.Event;
import javafx.geometry.Point2D;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.util.Duration;
import javafx.util.converter.DefaultStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,12 +38,23 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
if(empty) {
setText(null);
setGraphic(null);
setTooltip(null);
} else {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
setText(label);
setContextMenu(new LabelContextMenu(entry, label));
double width = label == null || label.length() < 20 ? 0.0 : TextUtils.computeTextWidth(getFont(), label, 0.0D);
if(width > getTableColumn().getWidth()) {
Tooltip tooltip = new Tooltip(label);
tooltip.setMaxWidth(getTreeTableView().getWidth());
tooltip.setWrapText(true);
setTooltip(tooltip);
} else {
setTooltip(null);
}
}
}
@ -47,6 +62,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
public void commitEdit(String label) {
if(label != null) {
label = label.trim();
if(label.length() > Persistable.MAX_LABEL_LENGTH) {
label = label.substring(0, Persistable.MAX_LABEL_LENGTH);
Platform.runLater(() -> {
Point2D p = this.localToScene(0.0, 0.0);
final Tooltip truncateTooltip = new Tooltip();
truncateTooltip.setText("Labels are truncated at " + Persistable.MAX_LABEL_LENGTH + " characters");
truncateTooltip.setAutoHide(true);
truncateTooltip.show(this, p.getX() + this.getScene().getX() + this.getScene().getWindow().getX() + this.getHeight(),
p.getY() + this.getScene().getY() + this.getScene().getWindow().getY() + this.getHeight());
PauseTransition pt = new PauseTransition(Duration.millis(2000));
pt.setOnFinished(_ -> truncateTooltip.hide());
pt.play();
});
}
}
// This block is necessary to support commit on losing focus, because
@ -103,7 +132,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
return confirmationsProperty;
}
private static class LabelContextMenu extends ContextMenu {
private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
@ -123,6 +152,13 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
}
});
getItems().add(pasteLabel);
MenuItem editLabel = new MenuItem("Edit Label...");
editLabel.setOnAction(AE -> {
hide();
startEdit();
});
getItems().add(editLabel);
}
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@ -57,7 +58,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
stage.setResizable(false);
StackPane scenePane = new StackPane();
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
if(OsType.getCurrent() == OsType.WINDOWS) {
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
}

View File

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
@ -9,7 +10,12 @@ import com.sparrowwallet.drongo.crypto.Bip322;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
@ -17,10 +23,11 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@ -32,17 +39,21 @@ import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
private final TextField address;
private final TextArea message;
private final TextArea signature;
@ -104,19 +115,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
this.wallet = wallet;
this.walletNode = walletNode;
final DialogPane dialogPane = getDialogPane();
final DialogPane dialogPane = new MessageSignDialogPane();
setDialogPane(dialogPane);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
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);
}
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
VBox vBox = new VBox();
vBox.setSpacing(20);
@ -132,6 +137,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
address.setTooltip(new Tooltip("Only singlesig addresses can sign"));
address.setSkin(new AddressTextFieldSkin(address));
addressField.getInputs().add(address);
if(walletNode != null) {
@ -154,6 +160,17 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
signature.setStyle("-fx-pref-height: 80px");
signature.setWrapText(true);
signature.setOnMouseClicked(event -> signature.selectAll());
ContextMenu signatureMenu = new ContextMenu();
MenuItem copyItem = new MenuItem("Copy");
copyItem.setOnAction(e -> signature.copy());
MenuItem pasteItem = new MenuItem("Paste");
pasteItem.setOnAction(e -> signature.paste());
MenuItem clearItem = new MenuItem("Clear");
clearItem.setOnAction(e -> signature.clear());
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
signature.setContextMenu(signatureMenu);
signatureField.getInputs().add(signature);
Field formatField = new Field();
@ -199,13 +216,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} else {
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
showQrButton.setDisable(wallet == null);
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
showQrButton.setGraphicTextGap(5);
showQrButton.setOnAction(event -> {
showQr();
});
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
signButton.setDisable(!canSign);
@ -244,12 +255,22 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
setFormatFromScriptType(address.getScriptType());
if(wallet != null) {
setWalletNodeFromAddress(wallet, address);
if(walletNode != null) {
setFormatFromScriptType(walletNode.getWallet().getScriptType());
}
}
} catch(InvalidAddressException e) {
//can't happen
}
}
});
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
if(wallet != null) {
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
}
});
}
EventManager.get().register(this);
@ -267,7 +288,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
Platform.runLater(() -> {
if(address.getText().isEmpty()) {
@ -277,7 +298,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
if(wallet != null && walletNode != null) {
setFormatFromScriptType(wallet.getScriptType());
setFormatFromScriptType(walletNode.getWallet().getScriptType());
} else {
formatGroup.selectToggle(formatElectrum);
}
@ -285,15 +306,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void checkWalletSigning(Wallet wallet) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
}
}
private boolean canSign(Wallet wallet) {
return wallet.getKeystores().get(0).hasPrivateKey()
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|| wallet.getKeystores().get(0).getWalletModel().isCard();
return wallet.getKeystores().getFirst().hasPrivateKey()
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
}
private boolean canSignAllFormats(Wallet wallet) {
return wallet.getKeystores().getFirst().hasPrivateKey();
}
private Address getAddress()throws InvalidAddressException {
@ -307,7 +332,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
} catch (InvalidAddressException e) {
return false;
}
@ -349,7 +374,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
//Note we can expect a single keystore due to the check in the constructor
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
@ -362,19 +387,25 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
Keystore keystore = decryptedWallet.getKeystores().getFirst();
String signatureText;
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
spendPrivKey.clear();
} else {
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
ECKey privKey = keystore.getKey(walletNode);
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
} else {
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
}
privKey.clear();
}
signature.clear();
signature.appendText(signatureText);
privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
@ -382,8 +413,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void signDeviceKeystore(Wallet deviceWallet) {
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
@ -450,11 +481,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@ -464,6 +495,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(isBip322()) {
showBip322Qr();
return;
}
//Note we can expect a single keystore due to the check in the constructor
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
@ -477,13 +513,88 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void showBip322Qr() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
byte[] psbtBytes = psbt.getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
scanQr();
}
}
private PSBT buildBip322Psbt(Wallet signingWallet) {
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
Keystore keystore = signingWallet.getKeystores().getFirst();
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
}
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
addBip322DerivationInfo(psbt, signingWallet);
return psbt;
}
private String extractBip322Signature(PSBT signedPsbt) {
String psbtMessage = signedPsbt.getGenericSignedMessage();
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
if(response.isEmpty() || response.get() != ButtonType.YES) {
return null;
}
}
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
}
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
}
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
ScriptType scriptType = signingWallet.getScriptType();
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
Keystore keystore = signingWallet.getKeystores().get(0);
ECKey pubKey = keystore.getPubKey(walletNode);
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
if(scriptType == ScriptType.P2TR) {
psbtInput.setTapInternalKey(pubKey);
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
} else {
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
}
}
private void scanQr() {
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.payload != null) {
if(result.psbt != null) {
try {
String sig = extractBip322Signature(result.psbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
} catch(Exception e) {
log.error("Error extracting BIP-322 signature from PSBT", e);
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
}
} else if(result.payload != null) {
signature.clear();
signature.appendText(result.payload);
} else if(result.exception != null) {
@ -495,6 +606,132 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void exportFile() {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
if(isBip322()) {
exportBip322File();
return;
}
StringJoiner joiner = new StringJoiner("\n");
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
//Note we can expect a single keystore due to the check in the constructor
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
joiner.add(walletNode.getWallet().getScriptType().toString());
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save Text File");
fileChooser.setInitialFileName("signmessage.txt");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
file = new File(file.getAbsolutePath() + ".txt");
}
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
writer.write(joiner.toString());
} catch(IOException e) {
log.error("Error saving signing message", e);
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
}
}
}
private void exportBip322File() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PSBT File");
fileChooser.setInitialFileName("bip322-signmessage.psbt");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(OutputStream os = new FileOutputStream(file)) {
os.write(psbt.getForExport().serialize());
} catch(IOException e) {
log.error("Error saving BIP-322 PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
}
}
}
private void importFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Signed File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
try {
byte[] psbtBytes = Files.readAllBytes(file.toPath());
PSBT signedPsbt = new PSBT(psbtBytes, false);
String sig = extractBip322Signature(signedPsbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
return;
} catch(Exception e) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
log.error("Error loading signed PSBT", e);
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
return;
}
//Fall through to text handling for non-.psbt files
}
}
try {
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
Matcher matcher = signedMessagePattern.matcher(content);
if(matcher.matches()) {
String signedMessage = matcher.group(1);
String signedAddress = matcher.group(2);
String signedSignature = matcher.group(3);
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
return;
} else if(!signedAddress.trim().equals(address.getText().trim())) {
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
return;
}
message.setText(signedMessage);
signature.setText(signedSignature);
} else {
signature.setText(content);
}
} catch(IOException e) {
log.error("Error loading signed message", e);
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
}
}
}
protected Glyph getSignGlyph() {
if(wallet != null) {
if(wallet.containsSource(KeystoreSource.HW_USB)) {
@ -539,4 +776,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
decryptWalletService.start();
}
}
private class MessageSignDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
SplitMenuButton signByButton = new SplitMenuButton();
signByButton.setText("Sign by QR");
signByButton.setDisable(wallet == null);
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
signByButton.setGraphicTextGap(5);
signByButton.setOnAction(event -> {
showQr();
});
MenuItem exportFile = new MenuItem("Sign by File...");
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
exportFile.setOnAction(event -> {
exportFile();
});
MenuItem importFile = new MenuItem("Load Signed File...");
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
importFile.setOnAction(event -> {
importFile();
});
signByButton.getItems().addAll(exportFile, importFile);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(signByButton, buttonData);
return signByButton;
}
return super.createButton(buttonType);
}
}
}

View File

@ -1,27 +1,11 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import java.util.Locale;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
@ -41,162 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
private void setMixSuccess(Utxo nextMixUtxo) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason.isError()) {
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) {
//Old error, don't set again.
return;
}
Glyph failGlyph = getFailGlyph();
setGraphic(failGlyph);
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
timeline.setOnFinished(event -> {
setTooltip(null);
});
timeline.play();
} else {
setContextMenu(null);
setGraphic(null);
setTooltip(null);
}
}
private String getPlatformSleepConfig() {
Platform platform = Platform.getCurrent();
if(platform == Platform.OSX) {
return "OSX System Preferences";
} else if(platform == Platform.WINDOWS) {
return "Windows Control Panel";
}
return "system power settings";
}
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
tt.setText(status);
setTooltip(tt);
} else {
setGraphic(null);
setTooltip(null);
}
}
private ProgressIndicator getProgressIndicator() {
ProgressIndicator progressIndicator;
if(getGraphic() instanceof ProgressIndicator) {
progressIndicator = (ProgressIndicator)getGraphic();
} else {
progressIndicator = new ProgressBar();
}
return progressIndicator;
}
private static Glyph getMixGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getStopGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
public static Glyph getFailGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.getStyleClass().add("fail-warning");
failGlyph.setFontSize(12);
return failGlyph;
}
private static class MixStatusContextMenu extends ContextMenu {
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(isMixing) {
MenuItem mixStop = new MenuItem("Stop Mixing");
if(pool != null) {
mixStop.disableProperty().bind(pool.mixingProperty().not());
}
mixStop.setGraphic(getStopGlyph());
mixStop.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mixStop(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixStop);
} else {
MenuItem mixNow = new MenuItem("Mix Now");
if(pool != null) {
mixNow.disableProperty().bind(pool.mixingProperty().not());
}
mixNow.setGraphic(getMixGlyph());
mixNow.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mix(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixNow);
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.sparrow.AppServices;
@ -49,8 +50,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
dialogPane.setGraphic(new ImageView(image));
dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
Grid grid = getGrid(emptyWordGrid);
@ -256,7 +256,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open PDF");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("PDF", "*.pdf")
);

View File

@ -1,24 +1,27 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.List;
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
private final DeterministicSeed.Type type;
public MnemonicKeystoreDisplayPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
this.type = keystore.getSeed().getType();
showWordList(keystore.getSeed());
}
@ -28,11 +31,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords / 3);
wordsPane = new GridPane();
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
@ -43,13 +44,20 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
int numCols = 3;
int numRows = Math.ceilDiv(numWords, numCols);
for(int i = 0; i < wordEntries.size(); i++) {
int col = i / numRows;
int row = i % numRows;
wordsPane.add(wordEntries.get(i), col, row);
}
vBox.getChildren().add(wordsPane);
@ -57,4 +65,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
stackPane.getChildren().add(vBox);
return stackPane;
}
@Override
protected WordlistProvider getWordlistProvider() {
return getWordListProvider(type);
}
}

View File

@ -19,7 +19,7 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
private boolean generated;
public MnemonicKeystoreEntryPane(String name, int numWords) {
super(name, "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
super(name, "Enter seed words", "", WalletModel.SEED);
showHideLink.setVisible(false);
buttonBox.getChildren().clear();

View File

@ -33,6 +33,7 @@ import java.util.Optional;
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected final Wallet wallet;
private final KeystoreMnemonicImport importer;
private final KeyDerivation defaultDerivation;
private SplitMenuButton importButton;
@ -43,10 +44,11 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private Button confirmButton;
private List<String> generatedMnemonicCode;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet;
this.importer = importer;
this.defaultDerivation = defaultDerivation;
createImportButton();
buttonBox.getChildren().add(importButton);
@ -56,10 +58,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.getStyleClass().add("default-button");
setDefaultButton(importButton);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
importKeystore(getDefaultDerivation(), false);
});
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
@ -77,6 +79,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
importButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
}
protected void enterMnemonic(int numWords) {
generatedMnemonicCode = null;
super.enterMnemonic(numWords);
@ -135,7 +141,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -243,14 +249,14 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(false);
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
setContent(getDerivationEntry(getDefaultDerivation()));
}
}
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);

View File

@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
@ -12,20 +15,15 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.*;
import javafx.util.Callback;
import javafx.util.Duration;
import org.controlsfx.control.textfield.AutoCompletionBinding;
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;
@ -41,7 +39,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
protected SplitMenuButton enterMnemonicButton;
protected TilePane wordsPane;
protected GridPane wordsPane;
protected Label validLabel;
protected Label invalidLabel;
@ -49,8 +47,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
protected IntegerProperty defaultWordSizeProperty;
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
super(title, description, content, imageUrl);
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
super(title, description, content, walletModel);
}
@Override
@ -111,23 +109,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
} else {
ScheduledService<Void> service = new ScheduledService<>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() {
return null;
}
};
}
};
service.setDelay(Duration.millis(500));
service.setOnSucceeded(event1 -> {
service.cancel();
AppServices.runAfterDelay(500, () -> {
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
});
service.start();
}
}
}
@ -153,6 +137,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
showWordList(words);
}
protected void showWordList(List<String> words) {
setContent(getMnemonicWordsEntry(words.size(), true, true));
setExpanded(true);
@ -174,11 +162,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3);
wordsPane = new GridPane();
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
@ -189,13 +175,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
int numCols = 3;
int numRows = Math.ceilDiv(numWords, numCols);
for(int i = 0; i < wordEntries.size(); i++) {
int col = i / numRows;
int row = i % numRows;
wordsPane.add(wordEntries.get(i), col, row);
}
vBox.getChildren().add(wordsPane);
@ -215,7 +208,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
buttonPane.getChildren().add(leftBox);
AnchorPane.setLeftAnchor(leftBox, 0.0);
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
@ -224,7 +217,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
@ -242,7 +235,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
empty = false;
}
if(!WordEntry.isValid(word)) {
if(!getWordlistProvider().isValid(word)) {
validWords = false;
}
}
@ -278,13 +271,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
//nothing by default
}
protected WordlistProvider getWordlistProvider() {
return getWordListProvider(DeterministicSeed.Type.BIP39);
}
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
}
protected static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
super();
setAlignment(Pos.CENTER_RIGHT);
@ -302,7 +302,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
for(int i = 0; i < words.length; i++) {
String word = words[i];
if(entry.nextField != null) {
if(i == words.length - 2 && isValid(word)) {
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
label.requestFocus();
} else {
entry.nextField.requestFocus();
@ -321,6 +321,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
};
wordField.setMaxWidth(100);
wordField.setAccessibleText("Word " + (wordNumber + 1));
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
// if text was added, fix the text to fit the requirements
@ -335,8 +336,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
});
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
@ -357,7 +357,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
));
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -378,28 +378,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
private final WordlistProvider wordlistProvider;
private final int wordNumber;
private final ObservableList<String> wordEntryList;
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
this.wordList = wordList;
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
this.wordlistProvider = wordlistProvider;
this.wordNumber = wordNumber;
this.wordEntryList = wordEntryList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
try {
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
if(!request.getUserText().isEmpty()) {
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
}
@ -412,7 +408,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
for(String word : wordlistProvider.getWordlist()) {
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
@ -424,7 +420,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
private boolean allPreviousWordsValid() {
for(int i = 0; i < wordEntryList.size() - 1; i++) {
if(!WordEntry.isValid(wordEntryList.get(i))) {
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
return false;
}
}
@ -485,17 +481,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("success");
validGlyph.setFontSize(12);
return validGlyph;
protected interface WordlistProvider {
List<String> getWordlist();
boolean isValid(String word);
boolean supportsPossibleLastWords();
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("failure");
invalidGlyph.setFontSize(12);
return invalidGlyph;
private static class Bip39WordlistProvider implements WordlistProvider {
@Override
public List<String> getWordlist() {
return Bip39MnemonicCode.INSTANCE.getWordList();
}
public boolean isValid(String word) {
return getWordlist().contains(word);
}
@Override
public boolean supportsPossibleLastWords() {
return true;
}
@Override
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
}
}
private static class Slip39WordlistProvider implements WordlistProvider {
@Override
public List<String> getWordlist() {
return Slip39MnemonicCode.INSTANCE.getWordList();
}
@Override
public boolean isValid(String word) {
return getWordlist().contains(word);
}
@Override
public boolean supportsPossibleLastWords() {
return false;
}
@Override
public List<String> getPossibleLastWords(List<String> previousWords) {
throw new UnsupportedOperationException();
}
}
}

View File

@ -0,0 +1,319 @@
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.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.slip39.Share;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
import com.sparrowwallet.sparrow.io.Slip39;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.*;
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
protected final Wallet wallet;
private final KeystoreMnemonicShareImport importer;
private final KeyDerivation defaultDerivation;
private final List<List<String>> mnemonicShares = new ArrayList<>();
private SplitMenuButton importButton;
private Button calculateButton;
private Button backButton;
private Button nextButton;
private int currentShare;
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet;
this.importer = importer;
this.defaultDerivation = defaultDerivation;
createImportButton();
buttonBox.getChildren().add(importButton);
}
@Override
protected Control createButton() {
createEnterMnemonicButton();
return enterMnemonicButton;
}
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Use 20 Words");
defaultWordSizeProperty = new SimpleIntegerProperty(20);
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
enterMnemonicButton.setText("Use " + newValue + " Words");
});
enterMnemonicButton.setOnAction(event -> {
resetShares();
enterMnemonic(defaultWordSizeProperty.get());
});
int[] numberWords = new int[] {20, 33};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
final int words = numberWords[i];
item.setOnAction(event -> {
resetShares();
defaultWordSizeProperty.set(words);
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
protected List<Node> createRightButtons() {
calculateButton = new Button("Create Keystore");
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
calculateButton.setVisible(false);
backButton = new Button("Back");
backButton.setOnAction(event -> {
lastShare();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Display the last share added"));
backButton.setVisible(currentShare > 0);
nextButton = new Button("Next");
nextButton.setOnAction(event -> {
nextShare();
});
nextButton.managedProperty().bind(nextButton.visibleProperty());
nextButton.setTooltip(new Tooltip("Add the next share"));
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
nextButton.setDefaultButton(true);
nextButton.setDisable(true);
return List.of(backButton, nextButton, calculateButton);
}
@Override
protected void enterMnemonic(int numWords) {
super.enterMnemonic(numWords);
setDescription("Enter existing share");
}
private void resetShares() {
currentShare = 0;
mnemonicShares.clear();
}
private void lastShare() {
currentShare--;
showWordList(mnemonicShares.get(currentShare));
}
private void nextShare() {
if(currentShare == mnemonicShares.size()) {
mnemonicShares.add(wordEntriesProperty.get());
} else {
mnemonicShares.set(currentShare, wordEntriesProperty.get());
}
currentShare++;
if(currentShare < mnemonicShares.size()) {
showWordList(mnemonicShares.get(currentShare));
} else {
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
}
setExpanded(true);
}
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
boolean validSet = false;
boolean complete = false;
if(!empty && validWords) {
try {
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
validChecksum = true;
List<List<String>> existing = new ArrayList<>(mnemonicShares);
if(currentShare >= mnemonicShares.size()) {
existing.add(wordEntriesProperty.get());
}
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
validSet = true;
complete = true;
} catch(MnemonicException e) {
invalidLabel.setText(e.getTitle());
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
} catch(Slip39.Slip39ProgressException e) {
validSet = true;
invalidLabel.setText(e.getTitle());
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException mnemonicException) {
invalidLabel.setText(mnemonicException.getTitle());
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
} else {
invalidLabel.setText("Import Error");
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
}
}
}
calculateButton.setVisible(complete);
backButton.setVisible(currentShare > 0 && !complete);
nextButton.setDisable(!validChecksum || !validSet);
validLabel.setVisible(complete);
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
invalidLabel.setVisible(!complete && !empty);
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
setDefaultButton(importButton);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(getDefaultDerivation(), false);
});
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation, false);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private List<ChildNumber> getDefaultDerivation() {
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
}
private void prepareImport() {
nextShare();
backButton.setVisible(false);
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
setExpanded(true);
enterMnemonicButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(false);
setContent(getDerivationEntry(getDefaultDerivation()));
}
}
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, mnemonicShares, passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
} catch (ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
errorMessage = "Invalid word list - checksum incorrect";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKeystore(importDerivation, false);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
public static Glyph getIncompleteGlyph() {
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
warningGlyph.getStyleClass().add("warn-icon");
warningGlyph.setFontSize(12);
return warningGlyph;
}
@Override
protected WordlistProvider getWordlistProvider() {
return getWordListProvider(DeterministicSeed.Type.SLIP39);
}
}

View File

@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
private Button importButton;
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.importer = importer;
}
@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected List<Node> createRightButtons() {
discoverButton = new Button("Discover Wallet");
discoverButton.setDisable(true);
discoverButton.setDefaultButton(true);
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setOnAction(event -> {
discoverWallet();
@ -66,6 +66,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
importButton = new Button("Import Wallet");
importButton.setDisable(true);
importButton.setDefaultButton(!AppServices.onlineProperty().get());
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
importButton.setOnAction(event -> {
@ -80,7 +81,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -107,14 +108,14 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
discoverButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
derivations.add(List.of(new ChildNumber(0, true)));
derivations.add(ScriptType.P2PKH.getDefaultDerivation(1)); //Bisq segwit misderivation
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
for(List<ChildNumber> derivation : derivations) {
try {
Wallet wallet = getWallet(scriptType, derivation);
Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation);
wallets.add(wallet);
} catch(ImportException e) {
String errorMessage = e.getMessage();
@ -133,15 +134,21 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
walletDiscoveryService.setOnSucceeded(successEvent -> {
discoverButton.setGraphic(null);
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
if(optWallet.isPresent()) {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
if(optWallets.isPresent()) {
List<Wallet> discoveredWallets = optWallets.get();
if(discoveredWallets.size() > 1) {
for(Wallet wallet : discoveredWallets) {
wallet.setName(wallet.getKeystores().getFirst().getLabel() + " " + wallet.getScriptType().getDescription());
}
}
EventManager.get().post(new WalletImportEvent(discoveredWallets));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
"Could not find an HD wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
@ -156,52 +163,61 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
walletDiscoveryService.start();
}
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
Wallet wallet = new Wallet("");
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
return wallet;
}
private Node getScriptTypeEntry() {
Label label = new Label("Script Type:");
Label label = new Label("Type:");
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
comboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
}
@Override
public ScriptType fromString(String string) {
public PolicyAndScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
comboBox.setMaxWidth(220);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importMnemonicButton = new Button("Import");
importMnemonicButton.setDefaultButton(true);
importMnemonicButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
ScriptType scriptType = scriptTypeComboBox.getValue();
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
PolicyAndScriptType type = comboBox.getValue();
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
EventManager.get().post(new WalletImportEvent(wallet));
} catch(ImportException e) {
log.error("Error importing mnemonic", e);
@ -223,4 +239,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -1,14 +1,14 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.tools.Platform;
public class NumberCell extends TreeTableCell<Entry, Number> {
public NumberCell() {
super();
getStyleClass().add("number-cell");
if(Platform.getCurrent() == Platform.OSX) {
if(OsType.getCurrent() == OsType.MACOS) {
getStyleClass().add("number-field");
}
}

View File

@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URL;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
this.paymentCodeProperty.set(paymentCode);
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
}
public void clearPaymentCode() {
this.paymentCodeProperty.set(null);
}
@ -128,8 +124,11 @@ public class PayNymAvatar extends StackPane {
log.debug("Requesting PayNym avatar from " + url);
}
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, true);
if(image.getException() != null) {
throw image.getException();
}
paymentCodeCache.put(cacheId, image);
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
return image;

View File

@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
linkButton.setDisable(true);
payNymController.linkPayNym(payNym);
});
if(payNymController.isSelectLinkedOnly()) {
getStyleClass().add("unlinked");
}
getStyleClass().add("unlinked");
}
}

View File

@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
setPaymentCodeString();
}
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
setPaymentCodeString();
}
private void setPaymentCodeString() {
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
setText(abbrevPcode);

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