Compare commits

...
243 Commits
Author SHA1 Message Date
Craig Raw a4e9ef989d darker dark theme and other styling improvements 2023-04-04 12:33:12 +02:00
Craig Raw 4ed8550f1d fix dark themed spreadsheets 2023-04-04 09:22:49 +02:00
Craig Raw 4bec71e7c4 read name from ur:crypto-hdkey and set keystore label if present 2023-03-29 16:00:32 +02:00
Craig Raw 961fd94dd6 allow stowaway counterparty to spend postmix utxos but receive to master wallet 2023-03-29 15:17:19 +02:00
Craig Raw 7915bbfa47 save border wallet pdf when generating a new recovery phrase 2023-03-29 12:00:04 +02:00
Craig Raw 49e70e8e9b fix accidentally making keystore fields editable on loading of single account non-watchonly wallets 2023-03-29 09:22:45 +02:00
Craig Raw 6063b02113 generate border wallets grid from seed words 2023-03-29 08:21:15 +02:00
Craig Raw faa5a11c94 update tor to 0.4.7.13 using kmp-tor library 2023-03-28 14:38:20 +02:00
Craig Raw acab50cdcd add diynodes.com public mainnet electrum server 2023-03-28 09:15:38 +02:00
Craig Raw 4d7d897e06 add port of deterministic prng for border wallets word shuffle 2023-03-28 08:37:20 +02:00
Craig Raw af532e7fc9 import seed via border wallets grid pattern 2023-03-27 11:00:32 +02:00
Craig Raw fd2b383dbc autosuggest possible words for the last word in a bip39 seed 2023-03-22 15:50:12 +02:00
Craig Raw 98b33e184e use whirlpool premix priority slider to change mixfeetarget, display warning if chosen fee rate is much lower than normal priority 2023-03-21 09:34:19 +02:00
Craig Raw 3bc7c7473a fix error initializing whirlpool on new wallet without a passphrase 2023-03-21 08:34:06 +02:00
Craig Raw 4f6981b869 change wallet gap limit and subscribe to new addresses if an address beyond gap limit range is requested 2023-03-15 08:44:53 +02:00
Craig Raw 258d46a253 support saving tapsigner backup as binary file 2023-03-09 13:40:57 +02:00
Craig Raw 40a3eb5d4f followup, minor tidying 2023-03-09 11:01:25 +02:00
craigrawandGitHub 35965235f3 Merge pull request #860 from wazint/master
Add context menu to copy amount values from amount cells
2023-03-09 10:55:36 +02:00
Craig Raw 368b24ea3b cormorant: handle empty (0 block only) chains 2023-03-09 10:09:52 +02:00
Craig Raw 107b5ba36c show psbt qrs without non witness utxo entries for segwit signing wallets 2023-03-07 13:17:33 +02:00
Craig Raw 84978a3d5d use different addresses when sending batched payments to the same paynym 2023-03-06 12:30:26 +02:00
Craig Raw dd3b980c36 strip path from server url when determining host and port 2023-03-06 10:40:46 +02:00
wazint b9a553abf2 don't initialise a ContextMenu for each cell update 2023-03-04 20:30:47 +02:00
Craig Raw 48b3dbc353 bump to v1.7.4 2023-03-01 10:43:36 +02:00
Craig Raw fb40d991bb replace default textinput dialog with custom textfield dialog 2023-03-01 08:14:25 +02:00
Craig Raw 5fe6a7196a use uppercase to encode pdf output descriptor qr 2023-02-28 16:52:42 +02:00
Craig Raw f06b859c82 extend request timeout for paynym api 2023-02-28 14:37:46 +02:00
Craig Raw 80dd59928e upgrade jlink plugin to support java 19 2023-02-28 14:21:57 +02:00
Craig Raw f22f76464a add option ot disable block explorer 2023-02-28 13:59:45 +02:00
Craig Raw dfe1f16495 configure a block explorer url, and open a txid in the configured block explorer 2023-02-28 13:19:24 +02:00
Craig Raw 90a9030ecb allow crypto-output qr scanning from wallet import dialog 2023-02-27 14:03:12 +02:00
Craig Raw 4ab33a373c reduce log level for no card reader errors 2023-02-27 12:36:41 +02:00
Craig Raw 10e751d6e1 upgrade to hwi 2.2.1 2023-02-27 12:18:50 +02:00
Craig Raw 5f40669af7 request confirmation before enabling use of a bip39 passphrase 2023-02-27 09:11:06 +02:00
wazint 97b4ed48db add context menu to copy amount values from amount cells 2023-02-24 16:55:40 +02:00
Craig Raw 9fc096569a add minfeerate parameter to bip78 payjoin urls 2023-02-23 12:53:12 +02:00
Craig Raw 41636f7152 support encrypted bip129 wallet imports 2023-02-23 12:17:41 +02:00
Craig Raw fc5d48de6f bip129 round 2 support (wallet import and export) 2023-02-23 12:02:06 +02:00
Craig Raw 2a7f14a4ed bip129 round 1 support with optional signing of bsms keystore exports 2023-02-22 10:22:04 +02:00
Craig Raw 1cb6778502 bump required java version to 18 2023-02-20 11:56:34 +02:00
Craig Raw 7f254e763d fix keystore encryption issue when changing the password on a wallet with freshly added accounts 2023-02-17 09:10:40 +02:00
Craig Raw e0ff42b6a4 terminal: add lock menu item to all wallets with a password 2023-02-17 08:37:10 +02:00
craigrawandGitHub 88fc8f5017 Merge pull request #843 from secondl1ght/master
add StartupWMClass to linux .desktop file
2023-02-15 08:00:36 +02:00
secondl1ghtandGitHub d7072928de add StartupWMClass to linux .desktop file 2023-02-14 21:22:41 -07:00
Craig Raw 0cc9ddba05 read and throw hwi error stream if stdout empty 2023-02-14 09:57:23 +02:00
Craig Raw e3799cd0a8 fix error receiving a stowaway to postmix by reverting to master wallet 2023-02-13 18:27:21 +02:00
Craig Raw ea6b30326e fix taproot signature hash for single | anyonecanpay 2023-02-13 17:35:17 +02:00
Craig Raw 38768885e2 show hwi signature verification errors, display strings encoded into scripts 2023-02-13 16:36:33 +02:00
Craig Raw c360177c31 use default ports for bitcoin core if absent, fix ux on changing port for an aliased server 2023-02-12 13:06:21 +02:00
Craig Raw e88ea0bac1 improve ux of bip39 wallet discovery on bitcoin core 2023-02-12 11:48:46 +02:00
Craig Raw a66b36c59c cormorant: switch bitcoind client from named to array parameters to support btc-rpc-proxy 2023-02-11 14:28:10 +02:00
Craig Raw fb3b674b65 recompile secp256k1 on osx 10.13.6 2023-02-10 09:23:02 +02:00
Craig Raw eff0e201f3 cormorant: only use proxy when connecting to onion addresses 2023-02-10 09:12:09 +02:00
Craig Raw 58d10cbba4 v1.7.3 2023-02-09 18:30:28 +02:00
Craig Raw 29bce8a9bc revert to ubuntu 20.04 runner 2023-02-09 17:34:19 +02:00
Craig Raw 67dcf69a78 v1.7.2 2023-02-09 13:18:30 +02:00
Craig Raw 7ad8a04bda improve legacy core wallet error messages 2023-02-09 13:14:03 +02:00
Craig Raw 24e75603c6 update categories for linux desktop installation 2023-02-09 10:53:45 +02:00
Craig Raw 545342dfb4 disable server toggle when no server is configured (url host is empty) 2023-02-09 10:29:19 +02:00
Craig Raw b15d6308bd write and parse both multipath and single descriptors in wallet output descriptor export 2023-02-09 09:06:30 +02:00
Craig Raw ff0c381437 cormorant: find cookie dir for non-mainnet networks 2023-02-08 14:10:20 +02:00
Craig Raw 555260e954 implement bip329 for importing and exporting wallet labels 2023-02-08 08:03:06 +02:00
Craig Raw 8d584d1c48 followup for aarch64 2023-02-07 09:13:32 +02:00
Craig Raw 967cf0cdfa try to locate pcsc library on linux before searching for card terminals 2023-02-06 16:44:34 +02:00
Craig Raw 41ba8455a0 cormorant: avoid importing wallets when testing connection, only show prune warning once per connection 2023-02-06 13:25:35 +02:00
Craig Raw d84f3bf887 add config property autoSwitchProxy to disable automatic proxy switching on failure, and improve tor connection failure message 2023-02-06 11:38:19 +02:00
Craig Raw 153815d9e3 indicate in ssl handshake warning that a certificate renewal may be the cause 2023-02-06 11:01:42 +02:00
Craig Raw 0250579445 tapsigner: change card backup to base64 2023-02-06 10:56:53 +02:00
Craig Raw 7590d786b5 fix copy address output script bytes to return entire scriptpubkey 2023-02-03 13:57:29 +02:00
Craig Raw 55809b7dc3 decrypt keystore before requesting passphrase to show masterfingerprint 2023-02-03 08:20:09 +02:00
Craig Raw 06026b0a09 further improvements on wallet importing wrt pruned nodes 2023-02-01 14:30:24 +02:00
Craig Raw 2cd64aa650 improve handling of scan dates earlier than core pruned date 2023-02-01 13:48:20 +02:00
Craig Raw 0b980f6ab5 satscard: retrieve private keys for previously used slots 2023-02-01 11:37:36 +02:00
Craig Raw 73dcef9fd1 followup: add or remove card option from pay to dropdown as reader becomes available 2023-02-01 10:07:59 +02:00
Craig Raw 4e3491ec64 tapsigner and satscard initialization fixes, satscard address and private key retrieval, core address scanning support 2023-02-01 09:39:49 +02:00
Craig Raw 176e440195 unseal satscard functionality added to sweep private key dialog 2023-01-31 09:30:53 +02:00
Craig Raw 300545b289 refactor cardapi to generic service 2023-01-30 14:47:00 +02:00
Craig Raw 057a9efb1f cormorant: fix initialisation of sent txes without txindex 2023-01-30 14:46:12 +02:00
Craig Raw 9edeff9aab cormorant: set wallet to load on bitcoind startup, check if loaded first 2023-01-30 12:28:07 +02:00
Craig Raw f938506a3f add tapsigner message signing support 2023-01-30 09:41:12 +02:00
Craig Raw 4fb8c5a61b add card scan to hwi enumeration and refactor device pane 2023-01-27 13:58:38 +02:00
Craig Raw 7a99c4a11a add tapsigner signing support and refactor card api 2023-01-27 10:39:29 +02:00
Craig Raw 6c13504644 implement card initialization functionality 2023-01-26 15:47:33 +02:00
Craig Raw 3ddf4ed4b2 add functionality for tapsigner backup and pin change 2023-01-26 13:00:25 +02:00
Craig Raw 6b59ff60ad initialize and import tapsigner as keystore 2023-01-25 14:19:22 +02:00
Craig Raw 7c64d689fd cormorant: threading and scan date initialization improvements 2023-01-20 12:56:25 +02:00
Craig Raw 4ad9cdedb6 add merge function for wallet transaction entry edge case 2023-01-19 14:12:35 +02:00
Craig Raw 276cb8aecb cormorant: support transaction.get without txindex, use step function to add bip47 addresses 2023-01-19 13:52:47 +02:00
Craig Raw e7ed82699c add jade multisig export to wallet export dialog 2023-01-18 13:25:39 +02:00
Craig Raw 68cd3673af upgrade to hwi 2.2.0, add support for entering empty passphrases to trezor one 2023-01-18 11:13:13 +02:00
Craig Raw 5f96570c07 request treetable focus after editing a label cell 2023-01-16 14:46:24 +02:00
Craig Raw 5147ee8aee prefer loading transaction inputs from existing wallet transactions 2023-01-16 14:13:15 +02:00
Craig Raw 3cc2981b72 followup 2023-01-11 14:14:31 +02:00
Craig Raw 8038298485 show lifehash for master fingerprint in settings and passphrase dialog 2023-01-11 14:01:41 +02:00
Craig Raw d1a1bd5751 fix persistence of renaming and deleting newly created wallet accounts 2023-01-09 09:57:35 +02:00
Craig Raw 63b7aef91e upgrade gradle to 7.6 2023-01-07 08:23:44 +02:00
Craig Raw 8a51a47156 upgrade gradle to 7.6 2023-01-06 10:49:26 +02:00
Craig Raw 827efe071e upgrade junit 2023-01-05 12:17:49 +02:00
Craig Raw 56784b684a allow expired certificates for electrum servers so long as they have been previously used or explicitly approved 2022-12-16 12:06:05 +02:00
Craig Raw 1fa52f043c add fee column to transactions csv for outgoing (spending) transactions 2022-12-15 17:04:01 +02:00
Craig Raw ce44cfe877 export 8 decimal places when exporting a csv in btc units 2022-12-15 16:37:33 +02:00
Craig Raw 8ba0a9f360 cormorant: increase descriptor wallet gap limit for postmix receive chain 2022-12-15 16:00:56 +02:00
Craig Raw 41dabac75b cormorant: syncing and pruning improvements 2022-12-15 14:03:56 +02:00
Craig Raw 064708f088 avoid copying wallet history unnecessarily on wallet load 2022-12-15 08:29:25 +02:00
Craig Raw 66dc394215 cormorant: send first scan event immediately 2022-12-14 16:29:16 +02:00
Craig Raw 5ca60699ef cormorant: improve scanning behaviour 2022-12-14 14:55:00 +02:00
Craig Raw 61d9ad1875 cormorant: rbf handling and related fixes 2022-12-12 12:55:19 +02:00
Craig Raw af6bbebac4 cormorant: add batching support 2022-12-12 11:33:51 +02:00
Craig Raw 6f4fc4f2ca avoid triggering all history changed event on unconfident script hash status calculations 2022-12-12 10:49:29 +02:00
Craig Raw 00f5001385 cormorant: fix scan date of nested wallet import 2022-12-12 10:09:22 +02:00
Craig Raw 3f3cdca94f refactor out unnecessary parameter 2022-12-12 09:14:17 +02:00
Craig Raw 08cf01a5c6 add cormorant server to support bitcoin core descriptor wallets 2022-12-08 08:42:40 +02:00
Craig Raw df7f40dbc9 followup 2022-12-07 11:00:16 +02:00
Craig Raw 12c1725260 fix edge case when loading wallets with matching tx inputs and outputs 2022-12-06 11:22:35 +02:00
Craig Raw aa8380eb03 add https protocol for bitcoin core connections over tls 2022-12-05 11:57:25 +02:00
Craig Raw 0e26f8fce1 add note on disabling derivation path validation to keystore help tooltip 2022-12-03 07:41:56 +02:00
Craig Raw 8de14dcbce improve handling of certain electrum server errors 2022-12-02 17:16:34 +02:00
Craig Raw 6871810c7c improve display of json rpc error exceptions 2022-12-01 09:39:47 +02:00
Craig Raw 6ac294920e improve encapsulation and binding lifecycle of cell confirmation listeners 2022-11-30 11:12:01 +02:00
Craig Raw 4b32eb397e add seedtool svg icons 2022-11-30 08:31:06 +02:00
Craig Raw b25297e8b9 fix sparrow export file extension to be always mv.db 2022-11-28 12:59:30 +02:00
Craig Raw ff90a2c3e6 add block height to terminal connected label 2022-11-28 09:43:08 +02:00
Craig Raw 3cbe8d1537 set initial focus to done button on terminal server test dialog 2022-11-21 16:21:30 +02:00
Craig Raw 9293b622a3 allow message sign/verify from nested segwit wallets 2022-11-21 16:09:59 +02:00
Craig Raw 6337e1cf7d update dependencies for guava, simple-json-rpc and dependants 2022-11-21 12:08:27 +02:00
craigrawandGitHub 3ff3fb29b0 Merge pull request #753 from lukechilds/patch-1
Update bitcoin.lukechilds.co to bitcoin.lu.ke
2022-11-21 09:09:37 +02:00
craigrawandGitHub 149d297193 Merge pull request #752 from BitcoinQnA/master
Updated Passport Single/Multisig Import and Export Instructions
2022-11-21 09:05:34 +02:00
Craig Raw 47f7b8870c fix cancel of create wallet in terminal 2022-11-21 08:51:09 +02:00
Luke ChildsandGitHub 7ce7d37da7 Update bitcoin.lukechilds.co to bitcoin.lu.ke 2022-11-19 03:30:36 +07:00
BitcoinQnAandGitHub b422c754d6 Update Passport Single-sig instructions
Suited to the updated Batch 2 UI.
2022-11-18 16:52:58 +00:00
BitcoinQnAandGitHub be6e9019dc Update Passport Multisig instructions
Suited to the updated Batch 2 UI.
2022-11-18 16:49:26 +00:00
Craig Raw 011bb86b5f fix negative space 2022-11-17 15:45:24 +02:00
Craig Raw 8e1163d3db add seedsigner svg icons 2022-11-17 15:37:04 +02:00
Craig Raw 83c8b1c8e6 use svg icons where possible 2022-11-17 14:06:27 +02:00
Craig Raw d44aecea90 v1.7.1 2022-11-17 10:23:26 +02:00
Craig Raw c9288ab25b change qr code density for ur encoding via qr dialog button 2022-11-17 10:19:04 +02:00
Craig Raw e39a2cb944 fix isconnecting state change 2022-11-17 08:08:50 +02:00
Craig Raw fb25edb51c avoid logging a socket closed error when the connection has been shutdown 2022-11-16 13:05:48 +02:00
Craig Raw 06ff0498d4 revert 29cd321 and add special case for mixed seed/watch only multisig wallets 2022-11-16 11:16:14 +02:00
Craig Raw 63b27e7054 hide spend and freeze context menu items on address cell in utxo table 2022-11-16 10:13:55 +02:00
Craig Raw 0260a12663 close connecting sockets and interrupt read thread on shutdown 2022-11-16 08:16:56 +02:00
Craig Raw a05fcba6d9 add inverted icons for dark theme 2022-11-14 12:45:13 +02:00
Craig Raw 5be5363f25 optimization followup 2022-11-14 11:40:00 +02:00
Craig Raw cc961b4eeb all walletconfig for wallet scope configuration variables 2022-11-14 11:00:26 +02:00
Craig Raw 7e7795196c bring window to front when restored after being minimized to tray 2022-11-09 12:37:55 +02:00
Craig Raw fd0fe1110d improve terminal resizing behaviour 2022-11-09 12:15:12 +02:00
Craig Raw ea64fa0f85 terminal - show receive address as qr code 2022-11-09 11:40:34 +02:00
Craig Raw 2972f1a4d7 fix export from settings tabs for new account by ensuring bidirectional links are restored on save 2022-11-08 15:51:59 +02:00
Craig Raw 6990b398c2 always use db format for sparrow exporter 2022-11-08 15:50:27 +02:00
Craig Raw a25b53bd44 add error message when connecting to bitcoin core with a taproot wallet 2022-11-08 10:36:56 +02:00
Craig Raw 871c503bc9 terminal - add mix selected functionality to broadcast premix transactions 2022-11-08 10:09:25 +02:00
Craig Raw b7992ae9e1 improve .deb control file and avoid dependence on xdg-utils when building sparrow-server debs 2022-11-07 10:08:50 +02:00
Craig Raw 0a8eb2fbb7 avoid triggering close wallet events when reordering tabs 2022-11-07 09:05:08 +02:00
Craig Raw 7863fb7632 delay wallet file deletion to allow for database compaction and show error on failure 2022-11-07 08:29:29 +02:00
Craig Raw 6481d83b0c avoid using locale for unit formatting 2022-11-02 11:40:39 +02:00
Craig Raw 402d9b14ec fix typos and clarify submodule update description 2022-11-02 08:41:50 +02:00
craigrawandGitHub feeb4f0ac0 Merge pull request #730 from RequestPrivacy/master
Add git pull & git submodule update --checkout
2022-11-02 08:35:59 +02:00
RequestPrivacyandGitHub 143eb3213e Formating & Addition of cd Command 2022-11-01 17:42:17 +01:00
RequestPrivacyandGitHub cd96fc1daa Add git pull & git submodule update --checkout 2022-11-01 14:04:22 +01:00
Craig Raw 73d9cd2e68 disable assistive technologies in windows to avoid runtime crash 2022-10-31 17:14:58 +02:00
Craig Raw 3faf817148 consider ip range 100.64.0.0/10 as local network addresses 2022-10-31 08:41:22 +02:00
Craig Raw 96c88b7472 fix npe in terminal for exchange rate updates without a btc rate 2022-10-29 08:33:55 +02:00
Craig Raw e2795c7ef3 fix potential npe selecting server in alias dialog 2022-10-28 09:41:31 +02:00
Craig Raw e7f6f7f3db add show version to command line args 2022-10-27 11:49:20 +02:00
Craig Raw dd9868c918 v1.7.0 2022-10-27 11:10:50 +02:00
Craig Raw c2d3afae59 fix exception when clearing a server alias 2022-10-27 09:34:32 +02:00
Craig Raw 04a516d56b improve error messaging for payjoin requests 2022-10-27 08:11:58 +02:00
Craig Raw b27709e96f show network in main tab header background when not using mainnet 2022-10-26 15:54:59 +02:00
Craig Raw ebb7d23a05 hide mix failed after timeout 2022-10-26 12:40:17 +02:00
Craig Raw 97d121244f add support for deprecating importers and exporters, and deprecate cobo vault 2022-10-26 12:28:21 +02:00
Craig Raw 60dbc8ed84 add cancel transaction via rbf to unconfirmed tx context menu 2022-10-26 11:33:36 +02:00
Craig Raw 29cd321724 avoid showing usb signing dialog for watch only keystores in multisig wallets 2022-10-26 08:31:36 +02:00
Craig Raw 467b834955 add jade as airgapped keystore importer 2022-10-25 14:41:10 +02:00
Craig Raw 3bfa5d3dc4 specify arch in release tarball name 2022-10-25 12:02:00 +02:00
Craig Raw f501f08e17 only build headless when specifically requested 2022-10-25 11:27:37 +02:00
Craig Raw 7d796369d6 upgrade and fix action 2022-10-25 10:45:48 +02:00
Craig Raw c572011578 followup 2022-10-25 10:23:17 +02:00
Craig Raw dbc1e7746b configure headless build 2022-10-25 10:04:03 +02:00
Craig Raw 9325a1968b explicitly detect java.awt.headless in build 2022-10-25 09:38:16 +02:00
Craig Raw 85166635b4 followup 2022-10-25 08:52:23 +02:00
Craig Raw ed69a86529 improve detection and handling on headless systems 2022-10-25 08:34:31 +02:00
Craig Raw ab2c77695b show warning dialog when a legacy multi output descriptor is entered 2022-10-24 16:50:08 +02:00
Craig Raw 6ad81e1228 suppress verbose whirlpool connection related logging errors 2022-10-24 15:31:42 +02:00
Craig Raw ff340c2449 followup to test taskbar user attention feature support 2022-10-24 14:43:53 +02:00
Craig Raw f2b0f8ca9e followup to test taskbar api platform support 2022-10-24 14:06:38 +02:00
Craig Raw 0c213294ad request user attention via taskbar when soroban communication requires action 2022-10-24 14:00:20 +02:00
Craig Raw 7cdb7319ee multisig backup dialog tweaks 2022-10-24 13:05:44 +02:00
Craig Raw d1ff8d6e3e fix show/edit descriptor button height 2022-10-24 12:51:21 +02:00
Craig Raw d9ddc74d73 disable rbf if allow unconfirmed preference is disabled 2022-10-24 08:18:17 +02:00
Craig Raw 603df6d0f6 terminal - fix add account encryption and normalize ui widths 2022-10-20 12:45:09 +02:00
Craig Raw cbf847a57f terminal - lock wallet 2022-10-19 18:41:23 +02:00
Craig Raw e8fb676a24 terminal - show seed 2022-10-19 18:19:49 +02:00
Craig Raw 273f3043fb terminal - add account 2022-10-19 18:04:55 +02:00
Craig Raw 8f165b05c7 terminal - create watch only wallet 2022-10-19 15:37:14 +02:00
Craig Raw 8eb092a8d6 terminal - create bip39 wallet 2022-10-19 09:44:44 +02:00
Craig Raw 8dd1850905 add settings dialog and other terminal improvements 2022-10-18 12:42:44 +02:00
Craig Raw 0fa6bd56e2 add interface enum 2022-10-17 13:52:40 +02:00
Craig Raw c4c581525a remove stdout appender in terminal mode 2022-10-17 13:30:21 +02:00
Craig Raw d6a3824690 add linux aarch64 hwi 2022-10-12 11:25:49 +02:00
Craig Raw 7dba141073 add linux aarch64 bwt 2022-10-12 10:56:24 +02:00
Craig Raw f2f6e639dc minor fixes 2022-10-11 15:40:50 +02:00
Craig Raw 78afc5e4d5 fix no recent wallets issue 2022-10-11 15:26:14 +02:00
Craig Raw b2d85b6c78 detect and configure build for headless environments 2022-10-11 15:12:40 +02:00
Craig Raw f3b0d37c54 replace and customize javafx plugin for headless environments 2022-10-11 15:07:43 +02:00
Craig Raw 6768ad2028 improve close timing of wallet loading dialog 2022-10-11 12:45:24 +02:00
Craig Raw 81cde4756a fix threading issue on terminal wallet load 2022-10-11 12:16:18 +02:00
Craig Raw 778564a954 refactor and rename launch and application classes 2022-10-11 11:41:49 +02:00
Craig Raw 1e4c8c3837 separate application from main 2022-10-10 16:37:35 +02:00
Craig Raw 22408103ea show additional inputs in tx diagram as labels or abbreviated txid:index 2022-10-10 09:55:38 +02:00
Craig Raw 383d3954d8 add linux aarch64 libsecp256k1 2022-10-06 14:35:49 +02:00
Craig Raw 77a4e4aa50 suppress unnecessary unchecked cast warning 2022-10-06 13:38:32 +02:00
Craig Raw 19dedfa070 implement terminal mode 2022-10-06 13:10:18 +02:00
Craig Raw 52696b014f import wallet from output descriptor pdf, ignore newline characters in output descriptor dialog 2022-09-26 13:48:49 +02:00
Craig Raw 8fb6de85f1 add unit format menu selection for alternative grouping and decimal separators 2022-09-21 11:36:13 +02:00
Craig Raw 8270eb71db install context menu handler for anywhere within transaction diagram 2022-09-16 15:08:03 +02:00
Craig Raw edcf12de5a follow up 2022-09-16 12:46:39 +02:00
Craig Raw da3399468c save transaction diagram as image through context menu on transaction label 2022-09-16 12:12:25 +02:00
Craig Raw 923c61fceb fix import of electrum wallet without keystore labels 2022-09-14 11:15:15 +02:00
Craig Raw d3d939889e increase maximum gap limit, but display warning when gap limit is over 999 2022-09-14 11:06:15 +02:00
Craig Raw 2b4d3fac6c recommend backup of output descriptor when saving new multisig wallets 2022-09-14 10:42:41 +02:00
Craig Raw 1f67692727 add support for configuring server aliases, and switching servers via the tools menu 2022-09-12 15:44:47 +02:00
Craig Raw bacbdb848b add move left and right context menu items to reorder wallet and transaction tabs 2022-09-06 12:42:18 +02:00
Craig Raw 51ba7fc4cf add context menu item to addresses table to spend all utxos for an address 2022-09-05 14:37:05 +02:00
Craig Raw 59efed9e42 verify signatures immediately after signing as per recommendation in bip340 2022-09-02 11:27:30 +02:00
craigrawandGitHub 193fea4b69 Merge pull request #675 from nyxnor/repro-docs
improve reproducible docs
2022-08-30 09:24:43 +02:00
nyxnor b5cc2ff4c3 improve reproducible docs 2022-08-29 16:58:05 +00:00
Craig Raw 252ec58065 fix typo in build doc 2022-08-29 08:51:19 +02:00
Craig Raw b8979ed8b0 use compact parameter to improve paynym search performance 2022-08-24 11:46:53 +02:00
Craig Raw c24f953e52 export all related wallets when exporting to electrum personal server 2022-08-23 08:48:35 +02:00
Craig Raw d139ca2706 add wallet export to electrum personal server config file 2022-08-22 14:33:03 +02:00
Craig Raw bd421e877a followup 2022-08-22 11:35:04 +02:00
Craig Raw 25e1250710 add context menu item in transaction diagram to show input and output addresses as qrs 2022-08-22 11:34:03 +02:00
Craig Raw fad1dad76e add trace logging to electrum server calls 2022-08-17 11:11:39 +02:00
Craig Raw b3bd42b8f6 add logging to all external api calls 2022-08-17 10:59:33 +02:00
Craig Raw 5aea538f09 cancel whirlpool startup service when disconnecting 2022-08-16 10:45:01 +02:00
Craig Raw 16755e3140 freeze and unfreeze any utxos from address cell context menu 2022-08-11 10:26:23 +02:00
Craig Raw cbfb7230a8 set transaction label on soroban collaborative transactions 2022-08-10 15:25:44 +02:00
Craig Raw e438389953 allow soroban initiator to try again if meeting request fails 2022-08-10 14:15:40 +02:00
Craig Raw 6534ccb07e accept output descriptor fragments in pubkey qr scanner 2022-08-10 12:31:43 +02:00
Craig Raw ca782dfc69 avoid resolution of onion hosts when creating proxied server socket addresses 2022-08-10 12:02:23 +02:00
Craig Raw f1a662ba8a fix name length on coldcard multisig export and update help text 2022-08-08 13:46:37 +02:00
Craig Raw 33fb2a38fc fix reproducibility issue by avoiding objcopy use during build, remove unnecessary java native commands from binaries 2022-08-08 11:23:29 +02:00
Craig Raw 8b22e057bf update documentation to reference java 18.0.1 as current release jdk 2022-08-04 14:35:59 +02:00
428 changed files with 19050 additions and 1683 deletions
+19 -8
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-2022, ubuntu-20.04, macos-12]
steps:
- uses: actions/checkout@v3
with:
@@ -19,12 +19,6 @@ jobs:
java-version: '18.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build with Gradle
run: ./gradlew jpackage
- name: Package zip distribution
@@ -34,9 +28,26 @@ jobs:
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Upload Artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: Sparrow Build - ${{ runner.os }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
- name: Headless build with Gradle
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true clean jpackage
- name: Package headless tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
- name: Rename Headless Artifacts
if: ${{ runner.os == 'Linux' }}
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
- name: Upload Headless Artifact
if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v3
with:
name: Sparrow Build - ${{ runner.os }} Headless
path: |
build/jpackage/*
!build/jpackage/Sparrow/
+9 -9
View File
@@ -16,8 +16,8 @@ or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 17 to be installed.
The release binaries are built with [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
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).
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
@@ -36,7 +36,7 @@ When updating to the latest HEAD
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
> Video documentation of your build process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
## Running
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
`./sparrow`
Java 17 or higher must be installed.
Java 18 or higher must be installed.
## Configuration
@@ -77,11 +77,11 @@ Note that if you are connecting to an Electrum server when using testnet, that s
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
Platform | Location
-------- | --------
OSX | ~/.sparrow
Linux | ~/.sparrow
Windows | %APPDATA%/Sparrow
| Platform | Location |
|----------| -------- |
| OSX | ~/.sparrow |
| Linux | ~/.sparrow |
| Windows | %APPDATA%/Sparrow |
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
+204 -56
View File
@@ -1,11 +1,13 @@
import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.13'
id 'org-openjfx-javafxplugin'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.25.0'
id 'org.beryx.jlink' version '2.26.0'
}
def sparrowVersion = '1.6.6'
def sparrowVersion = '1.7.4'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -13,10 +15,26 @@ if(os.macOsX) {
}
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.2'
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}"
@@ -49,7 +67,7 @@ dependencies {
exclude group: 'org.hamcrest'
exclude group: 'junit'
}
implementation('com.google.guava:guava:28.2-jre')
implementation('com.google.guava:guava:31.1-jre')
implementation('com.google.code.gson:gson:2.8.6')
implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:4.0.3')
@@ -64,11 +82,11 @@ dependencies {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('com.github.arteam:simple-json-rpc-core:1.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
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'
}
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.4')
@@ -79,10 +97,15 @@ dependencies {
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
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('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
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.6.4')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@@ -98,18 +121,22 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.36')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.27')
testImplementation('junit:junit:4.12')
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('com.sparrowwallet:toucan:0.9.0')
testImplementation('junit:junit:4.13.1')
}
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.MainApp'
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
}
compileJava {
@@ -151,13 +178,19 @@ run {
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=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.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
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"]
}
if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
}
}
jlink {
@@ -177,7 +210,7 @@ jlink {
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
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/*']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
@@ -195,9 +228,11 @@ jlink {
"--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-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
@@ -205,11 +240,18 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
if(os.windows) {
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
}
if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
}
}
addExtraDependencies("javafx")
jpackage {
@@ -225,7 +267,12 @@ jlink {
installerType = "exe"
}
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
if(headless) {
installerOptions = ['--license-file', 'LICENSE']
} else {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--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']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
@@ -251,7 +298,7 @@ task packageZipDistribution(type: Zip) {
task packageTarDistribution(type: Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP
from("$buildDir/jpackage/") {
@@ -260,7 +307,7 @@ task packageTarDistribution(type: Tar) {
}
extraJavaModuleInfo {
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
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')
@@ -275,11 +322,11 @@ extraJavaModuleInfo {
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
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.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
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')
@@ -313,7 +360,7 @@ extraJavaModuleInfo {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
@@ -321,7 +368,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
@@ -329,7 +376,7 @@ extraJavaModuleInfo {
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
@@ -376,16 +423,11 @@ extraJavaModuleInfo {
requires('java.xml')
requires('java.logging')
}
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
exports('mu')
requires('kotlin.stdlib')
requires('org.slf4j')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
module('guava-31.1-jre.jar', 'com.google.common', '31.1-jre') {
exports('com.google.common.eventbus')
exports('com.google.common.net')
exports('com.google.common.base')
@@ -456,13 +498,6 @@ extraJavaModuleInfo {
requires('javafx.graphics')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
@@ -470,8 +505,10 @@ extraJavaModuleInfo {
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
module('nightjar-0.2.36.jar', 'com.sparrowwallet.nightjar', '0.2.36') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
@@ -559,29 +596,140 @@ extraJavaModuleInfo {
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
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('kotlinx-coroutines-core-jvm-1.6.4.jar', 'kotlinx.coroutines.core.jvm', '1.6.4') {
exports('kotlinx.coroutines')
requires('kotlin.stdlib')
requires('commons.compress')
requires('org.tukaani.xz')
requires('java.instrument')
uses('kotlinx.coroutines.CoroutineExceptionHandler')
uses('kotlinx.coroutines.internal.MainDispatcherFactory')
}
module('kotlinx-coroutines-javafx-1.6.4.jar', 'kotlinx.coroutines.javafx', '1.6.4') {
exports('kotlinx.coroutines.javafx')
requires('kotlinx.coroutines.core.jvm')
requires('kotlin.stdlib')
requires('javafx.graphics')
}
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.jvm')
requires('java.management')
requires('io.github.microutils.kotlin.logging')
}
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
exports('net.freehaven.tor.control')
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('commons-compress-1.18.jar', 'commons.compress', '1.18') {
exports('org.apache.commons.compress')
requires('org.tukaani.xz')
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('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
exports('org.tukaani.xz')
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.jvm')
requires('kotlinx.atomicfu')
requires('kmp.tor.controller.jvm')
requires('kmp.tor.common.jvm')
}
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
exports('com.runjva.sourceforge.jsocks.protocol')
requires('org.slf4j')
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.jvm')
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.jvm')
}
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("encoding-base16-jvm-1.2.1.jar", 'encoding.base16.jvm', "1.2.1") {
exports('io.matthewnelson.encoding.base16')
requires('encoding.core.jvm')
requires('kotlin.stdlib')
}
module("encoding-base32-jvm-1.2.1.jar", 'encoding.base32.jvm', "1.2.1")
module("encoding-base64-jvm-1.2.1.jar", 'encoding.base64.jvm', "1.2.1")
module("encoding-core-jvm-1.2.1.jar", 'encoding.core.jvm', "1.2.1") {
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('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
+9
View File
@@ -4,10 +4,15 @@ plugins {
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
implementation 'org.javamodularity:moduleplugin:1.8.12'
}
repositories {
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
gradlePlugin {
@@ -17,5 +22,9 @@ gradlePlugin {
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"
}
}
}
@@ -0,0 +1,114 @@
/*
* 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;
}
}
@@ -0,0 +1,164 @@
/*
* 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()));
}
}
}
@@ -0,0 +1,91 @@
/*
* 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 )
);
}
}
@@ -0,0 +1,49 @@
/*
* 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);
}
}
@@ -0,0 +1,124 @@
/*
* 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()));
}
}
+83 -25
View File
@@ -1,68 +1,125 @@
## Reproducible builds
# Reproducible builds
Reproducibility is a goal of the Sparrow Wallet project.
Reproducibility is a goal of the Sparrow Wallet project.
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
Work on resolving both of these issues is ongoing.
### Reproducing a release
## Reproducing a release
#### Install Java
### 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.4 and later, this is Eclipse Temurin 17.0.2+8.
It is available for all supported platforms from [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
#### 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).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_windows_hotspot_17.0.2_8.zip)
- [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)
#### Java from Adoptium deb repo
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
```shell
sudo apt-get install -y wget apt-transport-https gnupg
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
echo "deb 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
- Install dependencies:
```sh
sudo apt-get install -y wget curl apt-transport-https gnupg
```
Download Adoptium public PGP key:
```sh
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
```
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
```
gpg --import --import-options show-only adoptium.asc
```
If key doesn't match, do not procede.
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-17-jdk=17.0.2+8
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).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 17.0.2-tem
sdk install java 18.0.1-tem
```
#### Other requirements
### Other requirements
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
```shell
sudo apt install -y rpm fakeroot binutils
```
#### Building the binaries
### Building the binaries
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.6.6"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
GIT_TAG="1.7.3"
```
Thereafter, building should be straightforward:
The project can then be initially cloned as follows:
```shell
cd sparrow
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
```
If you already have the sparrow repo cloned, fetch all new updates and checkout the release. For this, change into your local sparrow folder and execute:
```shell
cd {yourPathToSparrow}/sparrow
git pull --recurse-submodules
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.
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:
```shell
git submodule update --checkout
```
Thereafter, building should be straightforward. If not already done, change into the sparrow folder and run:
```shell
cd {yourPathToSparrow}/sparrow # if you aren't already in the sparrow folder
./gradlew jpackage
```
The binaries (and installers) will be placed in the `build/jpackage` folder.
#### Verifying the binaries are identical
### Verifying the binaries are identical
Verify the built binaries against the released binaries on https://github.com/sparrowwallet/sparrow/releases.
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
@@ -74,3 +131,4 @@ diff -r build/jpackage/Sparrow /tmp/Sparrow
This command should have no output indicating that the two folders (and all their contents) are identical.
If there is output, please open an issue with detailed instructions to reproduce, including build system platform.
+1 -1
Submodule drongo updated: 8cdea77562...fe19c86544
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+8 -4
View File
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,10 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
Vendored
+1
View File
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -5,5 +5,6 @@ Exec=/opt/sparrow/bin/Sparrow %U
Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Unknown
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
Categories=Finance;Network;
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
StartupWMClass=Sparrow
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.6.6</string>
<string>1.7.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -12,7 +12,7 @@ public class AboutController {
private Label title;
public void initializeView() {
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
title.setText(SparrowWallet.APP_NAME + " " + SparrowWallet.APP_VERSION + SparrowWallet.APP_VERSION_SUFFIX);
}
public void setStage(Stage stage) {
@@ -35,7 +35,6 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
@@ -132,6 +131,9 @@ public class AppController implements Initializable {
@FXML
private ToggleGroup bitcoinUnit;
@FXML
private ToggleGroup unitFormat;
@FXML
private ToggleGroup theme;
@@ -182,6 +184,9 @@ public class AppController implements Initializable {
@FXML
private MenuItem showPayNym;
@FXML
private Menu switchServer;
@FXML
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@@ -217,6 +222,41 @@ public class AppController implements Initializable {
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
};
private final ListChangeListener<Tab> tabsChangeListener = (c) -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
if(c.wasRemoved() && previouslySelectedTab != null) {
tabs.getSelectionModel().select(previouslySelectedTab);
}
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData) tab.getUserData()).getType() == TabData.TabType.WALLET);
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData) tab.getUserData()).getType() == TabData.TabType.WALLET);
if(walletAdded || walletRemoved) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
.flatMap(tab -> ((TabPane) tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData) subTab.getUserData())).collect(Collectors.toList());
if(!closedWalletTabs.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
}
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData) tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData) tabData).collect(Collectors.toList());
if(!closedTransactionTabs.isEmpty()) {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
closeTab.setDisable(tabs.getTabs().isEmpty());
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage) tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
exportWallet.setDisable(true);
}
}
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -273,40 +313,7 @@ public class AppController implements Initializable {
//Draggle tabs introduce unwanted movement when selecting between them
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
if(c.wasRemoved() && previouslySelectedTab != null) {
tabs.getSelectionModel().select(previouslySelectedTab);
}
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
if(walletAdded || walletRemoved) {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
.flatMap(tab -> ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData)subTab.getUserData())).collect(Collectors.toList());
if(!closedWalletTabs.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
}
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData)tabData).collect(Collectors.toList());
if(!closedTransactionTabs.isEmpty()) {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
closeTab.setDisable(tabs.getTabs().isEmpty());
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage)tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
exportWallet.setDisable(true);
}
}
});
tabs.getTabs().addListener(tabsChangeListener);
tabs.getScene().getWindow().setOnCloseRequest(event -> {
EventManager.get().unregister(this);
@@ -322,6 +329,15 @@ public class AppController implements Initializable {
Optional<Toggle> selectedUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
selectedUnitToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
UnitFormat format = Config.get().getUnitFormat();
if(format == null) {
format = UnitFormat.DOT;
Config.get().setUnitFormat(format);
}
final UnitFormat selectedFormat = format;
Optional<Toggle> selectedFormatToggle = unitFormat.getToggles().stream().filter(toggle -> selectedFormat.equals(toggle.getUserData())).findFirst();
selectedFormatToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
Theme configTheme = Config.get().getTheme();
if(configTheme == null) {
configTheme = Theme.LIGHT;
@@ -367,9 +383,10 @@ public class AppController implements Initializable {
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
});
configureSwitchServer();
setServerType(Config.get().getServerType());
serverToggle.setSelected(isConnected());
serverToggle.setDisable(Config.get().getServerType() == null);
serverToggle.setDisable(!Config.get().hasServer());
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
@@ -377,6 +394,7 @@ public class AppController implements Initializable {
});
openTransactionIdItem.disableProperty().bind(onlineProperty().not());
setNetworkLabel();
}
private void setPlatformApplicationMenu() {
@@ -386,10 +404,10 @@ public class AppController implements Initializable {
MenuItem preferences = new MenuItem("Preferences...");
preferences.setOnAction(this::openPreferences);
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
preferences, new SeparatorMenuItem(),
tk.createHideMenuItem(MainApp.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(MainApp.APP_NAME));
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
tk.setApplicationMenu(defaultApplicationMenu);
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
@@ -404,12 +422,27 @@ public class AppController implements Initializable {
}
}
private void setNetworkLabel() {
if(Network.get() != Network.MAINNET) {
Platform.runLater(() -> {
StackPane tabBackground = (StackPane)tabs.lookup(".tab-header-background");
if(tabBackground != null) {
HBox hBox = new HBox();
Label label = new Label(Network.get().toDisplayString());
label.setPadding(new Insets(0, 10, 0, 0));
hBox.getChildren().add(label);
hBox.setAlignment(Pos.CENTER_RIGHT);
tabBackground.getChildren().add(hBox);
}
});
}
}
public void showIntroduction(ActionEvent event) {
WelcomeDialog welcomeDialog = new WelcomeDialog();
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
preferencesDialog.showAndWait();
openPreferences(PreferenceGroup.SERVER);
}
}
@@ -450,7 +483,7 @@ public class AppController implements Initializable {
}
Stage stage = new Stage(StageStyle.UNDECORATED);
stage.setTitle("About " + MainApp.APP_NAME);
stage.setTitle("About " + SparrowWallet.APP_NAME);
stage.initOwner(tabs.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.setResizable(false);
@@ -720,7 +753,7 @@ public class AppController implements Initializable {
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
writer.flush();
} else {
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs));
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
}
} catch(IOException e) {
log.error("Error saving PSBT", e);
@@ -855,6 +888,13 @@ public class AppController implements Initializable {
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
}
public void setUnitFormat(ActionEvent event) {
MenuItem item = (MenuItem)event.getSource();
UnitFormat format = (UnitFormat)item.getUserData();
Config.get().setUnitFormat(format);
EventManager.get().post(new UnitFormatChangedEvent(format));
}
public void preventSleep(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setPreventSleep(item.isSelected());
@@ -902,8 +942,10 @@ public class AppController implements Initializable {
private String getServerToggleTooltipText(Integer currentBlockHeight) {
if(AppServices.isConnected()) {
return "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
return "Connected to " + Config.get().getServerDisplayName() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : "");
} else if(AppServices.isConnecting()) {
return "Connecting...";
}
return "Disconnected";
@@ -996,7 +1038,7 @@ public class AppController implements Initializable {
} catch(Exception e) {
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + SparrowWallet.APP_NAME + " already running?");
} else if(!attemptImportWallet(file, null)) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
@@ -1006,8 +1048,7 @@ public class AppController implements Initializable {
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
try {
checkWalletNetwork(walletAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
if(!walletAndKey.getWallet().isValid()) {
throw new IllegalStateException("Wallet file is not valid.");
}
@@ -1026,103 +1067,26 @@ public class AppController implements Initializable {
}
}
private void checkWalletNetwork(Wallet wallet) {
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
}
}
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
if(wallet.containsMasterPrivateKeys()) {
//Derive xpub and master fingerprint from seed, potentially with passphrase
Wallet copy = wallet.copy();
for(int i = 0; i < copy.getKeystores().size(); i++) {
Keystore copyKeystore = copy.getKeystores().get(i);
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
if(copyKeystore.getSeed().needsPassphrase()) {
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
} else {
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
if(optionalPassphrase.isPresent()) {
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
} else {
return;
}
}
} else {
copyKeystore.getSeed().setPassphrase("");
}
}
}
if(wallet.isEncrypted()) {
if(key == null) {
throw new IllegalStateException("Wallet was not encrypted, but seed is");
}
copy.decrypt(key);
}
if(wallet.isWhirlpoolMasterWallet()) {
String walletId = storage.getWalletId(wallet);
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
soroban.setHDWallet(copy);
}
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
}
for(int i = 0; i < wallet.getKeystores().size(); i++) {
Keystore keystore = wallet.getKeystores().get(i);
if(keystore.hasSeed()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
copyKeystore.getSeed().clear();
} else if(keystore.hasMasterPrivateExtendedKey()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
copyKeystore.getMasterPrivateKey().clear();
}
}
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isBip47()) {
try {
Keystore masterKeystore = wallet.getKeystores().get(0);
Keystore keystore = childWallet.getKeystores().get(0);
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
} catch(Exception e) {
log.error("Cannot prepare BIP47 keystore", e);
}
}
}
}
public void importWallet(ActionEvent event) {
WalletImportDialog dlg = new WalletImportDialog();
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
Optional<Wallet> optionalWallet = dlg.showAndWait();
if(optionalWallet.isPresent()) {
Wallet wallet = optionalWallet.get();
addImportedWallet(wallet);
List<WalletTabData> walletTabData = getOpenWalletTabData();
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
.map(WalletTabData::getWalletForm)
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
if(optNewWalletForm.isPresent()) {
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
selectTab(optNewWalletForm.get().getWallet());
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
addImportedWallet(wallet);
}
}
}
@@ -1136,6 +1100,10 @@ public class AppController implements Initializable {
new KeystoneSinglesig(), new KeystoneMultisig(),
new CaravanMultisig());
for(WalletImport importer : walletImporters) {
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
continue;
}
try(FileInputStream inputStream = new FileInputStream(file)) {
if(importer.isEncrypted(file) && password == null) {
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
@@ -1160,7 +1128,7 @@ public class AppController implements Initializable {
}
private void addImportedWallet(Wallet wallet) {
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName());
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
if(optNameAndBirthDate.isPresent()) {
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
@@ -1212,14 +1180,12 @@ public class AppController implements Initializable {
try {
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, null);
storage.restorePublicKeysFromSeed(wallet, null);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, null);
storage.restorePublicKeysFromSeed(childWallet, null);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
@@ -1239,8 +1205,7 @@ public class AppController implements Initializable {
wallet.encrypt(key);
storage.setEncryptionPubKey(encryptionPubKey);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, key);
storage.restorePublicKeysFromSeed(wallet, key);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
@@ -1248,8 +1213,7 @@ public class AppController implements Initializable {
childWallet.encrypt(key);
}
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key);
storage.restorePublicKeysFromSeed(childWallet, key);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
@@ -1284,13 +1248,18 @@ public class AppController implements Initializable {
}
public void openPreferences(ActionEvent event) {
PreferencesDialog preferencesDialog = new PreferencesDialog();
preferencesDialog.showAndWait();
openPreferences(PreferenceGroup.GENERAL);
}
public void openServerPreferences(ActionEvent event) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
openPreferences(PreferenceGroup.SERVER);
}
private void openPreferences(PreferenceGroup preferenceGroup) {
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
preferencesDialog.showAndWait();
configureSwitchServer();
serverToggle.setDisable(!Config.get().hasServer());
}
public void signVerifyMessage(ActionEvent event) {
@@ -1511,11 +1480,10 @@ public class AppController implements Initializable {
wallet.setName(name);
}
Tab tab = new Tab("");
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
glyph.setFontSize(10.0);
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
WalletIcon walletIcon = new WalletIcon(storage, wallet);
walletIcon.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
Label tabLabel = new Label(name);
tabLabel.setGraphic(glyph);
tabLabel.setGraphic(walletIcon);
tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel);
tab.setClosable(true);
@@ -1647,7 +1615,7 @@ public class AppController implements Initializable {
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
subTab.setUserData(tabData);
if(!wallet.isWhirlpoolChildWallet()) {
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
subTab.setContextMenu(getSubTabContextMenu(storage, wallet, subTabs, subTab));
}
subTabs.getTabs().add(subTab);
@@ -1658,6 +1626,10 @@ public class AppController implements Initializable {
});
subTabs.getSelectionModel().select(subTab);
if(wallet.isValid()) {
Platform.runLater(() -> walletForm.refreshHistory(AppServices.getCurrentBlockHeight()));
}
return walletForm;
} catch(IOException e) {
throw new RuntimeException(e);
@@ -1696,6 +1668,19 @@ public class AppController implements Initializable {
return null;
}
public List<WalletForm> getSelectedWalletForms() {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
if(selectedTab != null) {
TabData tabData = (TabData) selectedTab.getUserData();
if(tabData instanceof WalletTabData) {
TabPane subTabs = (TabPane) selectedTab.getContent();
return subTabs.getTabs().stream().map(subTab -> ((WalletTabData) subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
}
}
return Collections.emptyList();
}
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
if(Utils.isBase64(string) && !Utils.isHex(string)) {
addTransactionTab(name, file, Base64.getDecoder().decode(string));
@@ -1860,6 +1845,31 @@ public class AppController implements Initializable {
contextMenu.getItems().addAll(lock);
}
MenuItem moveRight = new MenuItem("Move Right");
moveRight.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab);
tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab);
tabs.getTabs().add(index + 1, tab);
tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab);
});
MenuItem moveLeft = new MenuItem("Move Left");
moveLeft.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab);
tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab);
tabs.getTabs().add(index - 1, tab);
tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab);
});
contextMenu.getItems().addAll(moveRight, moveLeft);
contextMenu.setOnShowing(event -> {
moveRight.setDisable(tabs.getTabs().indexOf(tab) == tabs.getTabs().size() - 1);
moveLeft.setDisable(tabs.getTabs().indexOf(tab) == 0);
});
MenuItem close = new MenuItem("Close");
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
@@ -1877,15 +1887,63 @@ public class AppController implements Initializable {
tabs.getTabs().removeAll(tabs.getTabs());
});
MenuItem delete = new MenuItem("Delete...");
delete.setOnAction(event -> {
deleteWallet(getSelectedWalletForm());
});
contextMenu.getItems().addAll(new SeparatorMenuItem(), close, closeOthers, closeAll);
if(tab.getUserData() instanceof WalletTabData walletTabData) {
Menu walletIcon = new Menu("Wallet Icon");
MenuItem custom = new MenuItem("Custom...");
custom.setOnAction(event -> {
setCustomIcon(walletTabData.getWallet());
});
MenuItem reset = new MenuItem("Reset");
reset.setOnAction(event -> {
resetIcon(walletTabData.getWalletForm());
});
walletIcon.getItems().addAll(custom, reset);
MenuItem delete = new MenuItem("Delete...");
delete.setOnAction(event -> {
deleteWallet(walletTabData.getWalletForm());
});
contextMenu.getItems().addAll(new SeparatorMenuItem(), walletIcon, delete);
}
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
return contextMenu;
}
private void setCustomIcon(Wallet wallet) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Image");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
try {
byte[] iconData = ImageUtils.resize(file, WalletIcon.SAVE_WIDTH, WalletIcon.SAVE_HEIGHT);
WalletConfig walletConfig = wallet.getMasterWalletConfig();
walletConfig.setIconData(iconData, true);
EventManager.get().post(new WalletConfigChangedEvent(wallet));
} catch(Exception e) {
log.error("Error creating custom wallet icon", e);
showErrorDialog("Error creating custom wallet icon", e.getMessage());
}
}
}
private void resetIcon(WalletForm walletForm) {
Wallet masterWallet = walletForm.getMasterWallet();
if(masterWallet.getWalletConfig() != null && masterWallet.getWalletConfig().isUserIcon()) {
masterWallet.getWalletConfig().setIconData(null, false);
EventManager.get().post(new WalletConfigChangedEvent(masterWallet));
}
}
private void deleteWallet(WalletForm selectedWalletForm) {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete " + selectedWalletForm.getWallet().getMasterName() + "?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
@@ -1930,13 +1988,30 @@ public class AppController implements Initializable {
private void deleteStorage(Storage storage) {
if(storage.isClosed()) {
Platform.runLater(storage::delete);
Platform.runLater(() -> {
Storage.DeleteWalletService deleteWalletService = new Storage.DeleteWalletService(storage);
deleteWalletService.setDelay(Duration.seconds(3));
deleteWalletService.setPeriod(Duration.hours(1));
deleteWalletService.setOnSucceeded(event -> {
deleteWalletService.cancel();
if(!deleteWalletService.getValue()) {
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
}
});
deleteWalletService.setOnFailed(event -> {
deleteWalletService.cancel();
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
});
deleteWalletService.start();
});
} else {
Platform.runLater(() -> deleteStorage(storage));
}
}
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
private ContextMenu getSubTabContextMenu(Storage storage, Wallet wallet, TabPane subTabs, Tab subTab) {
String walletId = storage.getWalletId(wallet);
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
rename.setOnAction(event -> {
@@ -1947,8 +2022,9 @@ public class AppController implements Initializable {
String label = optLabel.get();
subTabLabel.setText(label);
wallet.setLabel(label);
EventManager.get().post(new WalletLabelChangedEvent(wallet));
Wallet renamedWallet = AppServices.get().getWallet(walletId);
renamedWallet.setLabel(label);
EventManager.get().post(new WalletLabelChangedEvent(renamedWallet));
}
});
contextMenu.getItems().add(rename);
@@ -1958,11 +2034,12 @@ public class AppController implements Initializable {
delete.setOnAction(event -> {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
Wallet deletedWallet = AppServices.get().getWallet(walletId);
EventManager.get().post(new WalletDeletedEvent(deletedWallet));
subTabs.getTabs().remove(subTab);
if(subTabs.getTabs().size() == 1) {
setSubTabsVisible(subTabs, areSubTabsVisible());
}
EventManager.get().post(new WalletDeletedEvent(wallet));
}
});
contextMenu.getItems().add(delete);
@@ -1971,6 +2048,48 @@ public class AppController implements Initializable {
return contextMenu;
}
private void configureSwitchServer() {
switchServer.getItems().clear();
Config config = Config.get();
if(config.getServerType() == ServerType.BITCOIN_CORE && config.getRecentCoreServers() != null && config.getRecentCoreServers().size() > 1) {
for(Server server : config.getRecentCoreServers()) {
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.BITCOIN_CORE, server));
}
} else if(config.getServerType() == ServerType.ELECTRUM_SERVER && config.getRecentElectrumServers() != null && config.getRecentElectrumServers().size() > 1) {
for(Server server : config.getRecentElectrumServers()) {
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.ELECTRUM_SERVER, server));
}
}
switchServer.setVisible(!switchServer.getItems().isEmpty());
}
private CheckMenuItem getSwitchServerMenuItem(ServerType serverType, Server server) {
CheckMenuItem checkMenuItem = new CheckMenuItem(server.getDisplayName());
boolean selected = (serverType == ServerType.BITCOIN_CORE ? server.equals(Config.get().getCoreServer()) : server.equals(Config.get().getElectrumServer()));
checkMenuItem.setSelected(selected);
checkMenuItem.setOnAction(event -> {
if(!selected) {
boolean online = onlineProperty().get();
onlineProperty().set(false);
if(serverType == ServerType.BITCOIN_CORE) {
Config.get().setCoreServer(server);
} else if(serverType == ServerType.ELECTRUM_SERVER) {
Config.get().setElectrumServer(server);
}
Platform.runLater(() -> {
onlineProperty().set(online);
configureSwitchServer();
});
} else {
checkMenuItem.setSelected(true);
}
});
return checkMenuItem;
}
public void setServerType(ServerType serverType) {
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
serverToggle.getStyleClass().add("public-server");
@@ -1984,7 +2103,7 @@ public class AppController implements Initializable {
serverToggle.getStyleClass().remove("core-server");
}
serverToggle.setDisable(false);
serverToggle.setDisable(!Config.get().hasServer());
}
public void setTheme(ActionEvent event) {
@@ -2033,8 +2152,8 @@ public class AppController implements Initializable {
private void tabLabelAddFailure(Tab tab) {
Label tabLabel = (Label)tab.getGraphic();
if(!tabLabel.getStyleClass().contains("failure")) {
tabLabel.getGraphic().getStyleClass().add("failure");
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
if(walletIcon.addFailure()) {
tabLabel.setTooltip(new Tooltip("Error loading transaction history from server"));
}
}
@@ -2067,7 +2186,8 @@ public class AppController implements Initializable {
private void tabLabelRemoveFailure(Tab tab) {
Label tabLabel = (Label)tab.getGraphic();
tabLabel.getGraphic().getStyleClass().remove("failure");
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
walletIcon.removeFailure();
tabLabel.setTooltip(null);
}
@@ -2104,6 +2224,14 @@ public class AppController implements Initializable {
} else {
tabs.getScene().getStylesheets().remove(darkCss);
}
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData) {
Label tabLabel = (Label)tab.getGraphic();
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
walletIcon.refresh();
}
}
}
@Subscribe
@@ -2184,6 +2312,9 @@ public class AppController implements Initializable {
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName());
Label tabLabel = (Label)walletTab.getGraphic();
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
walletIcon.setWallet(event.getWallet());
}
}
}
@@ -2399,11 +2530,12 @@ public class AppController implements Initializable {
@Subscribe
public void connection(ConnectionEvent event) {
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
String status = "Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight();
statusUpdated(new StatusEvent(status));
setServerToggleTooltip(event.getBlockHeight());
serverToggleStopAnimation();
setTorIcon();
configureSwitchServer();
}
@Subscribe
@@ -2493,6 +2625,50 @@ public class AppController implements Initializable {
}
}
@Subscribe
public void cormorantSyncStatus(CormorantSyncStatusEvent event) {
serverToggle.setDisable(false);
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
statusUpdated(new StatusEvent("Syncing... (" + event.getProgress() + "% complete, synced to " + event.getTipAsString() + ")"));
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
statusBar.setProgress((double)event.getProgress() / 100);
}
}
}
@Subscribe
public void cormorantScanStatus(CormorantScanStatusEvent event) {
serverToggle.setDisable(true);
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
statusUpdated(new StatusEvent("Scanning... (" + event.getProgress() + "% complete" + (event.getRemainingAsString().isEmpty() ? ")" : ", " + event.getRemainingAsString() + " remaining)")));
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
statusBar.setProgress((double)event.getProgress() / 100);
}
} else if(event.isCompleted()) {
serverToggle.setDisable(false);
if(statusBar.getText().startsWith("Scanning...")) {
statusBar.setText("");
}
}
}
@Subscribe
public void cormorantPruneStatus(CormorantPruneStatusEvent event) {
if(event.legacyWalletExists()) {
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".\n\n" +
"Do you want to try using the existing legacy Bitcoin Core wallet?", ButtonType.YES, ButtonType.NO);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
Config.get().setUseLegacyCoreWallet(true);
onlineProperty().set(false);
Platform.runLater(() -> onlineProperty().set(true));
}
} else {
AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".");
}
}
@Subscribe
public void bwtBootStatus(BwtBootStatusEvent event) {
serverToggle.setDisable(true);
@@ -2623,6 +2799,12 @@ public class AppController implements Initializable {
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
}
@Subscribe
public void unitFormatChanged(UnitFormatChangedEvent event) {
Optional<Toggle> selectedToggle = unitFormat.getToggles().stream().filter(toggle -> event.getUnitFormat().equals(toggle.getUserData())).findFirst();
selectedToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
}
@Subscribe
public void openWalletsInNewWindowsStatusChanged(OpenWalletsNewWindowsStatusEvent event) {
openWalletsInNewWindows.setSelected(event.isOpenWalletsInNewWindows());
@@ -2704,4 +2886,17 @@ public class AppController implements Initializable {
lockAllWallets.setDisable(false);
}
}
@Subscribe
public void walletConfigChanged(WalletConfigChangedEvent event) {
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData walletTabData) {
if(walletTabData.getWallet() == event.getWallet()) {
Label tabLabel = (Label)tab.getGraphic();
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
walletIcon.refresh();
}
}
}
}
}
@@ -20,7 +20,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.sparrow.control.TextUtils;
import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
@@ -28,6 +27,7 @@ import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -42,7 +42,6 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
@@ -51,8 +50,6 @@ import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
import org.berndpruenster.netlayer.tor.Tor;
import org.controlsfx.control.HyperlinkLabel;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -69,8 +66,6 @@ import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class AppServices {
@@ -97,9 +92,11 @@ public class AppServices {
private final SorobanServices sorobanServices = new SorobanServices();
private InteractionServices interactionServices;
private static PayNymService payNymService;
private final MainApp application;
private final Application application;
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
@@ -173,8 +170,9 @@ public class AppServices {
openFiles(event.getFiles(), null);
};
public AppServices(MainApp application) {
private AppServices(Application application, InteractionServices interactionServices) {
this.application = application;
this.interactionServices = interactionServices;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
@@ -203,7 +201,7 @@ public class AppServices {
private void restartServices() {
Config config = Config.get();
if(config.hasServerAddress()) {
if(config.hasServer()) {
restartService(connectionService);
}
@@ -211,7 +209,7 @@ public class AppServices {
restartService(ratesService);
}
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET && Interface.get() == Interface.DESKTOP) {
restartService(versionCheckService);
}
@@ -253,7 +251,7 @@ public class AppServices {
}
if(Tor.getDefault() != null) {
Tor.getDefault().shutdown();
Tor.getDefault().getTorManager().destroy(true, success -> {});
}
}
@@ -268,7 +266,7 @@ public class AppServices {
connectionService.setOnRunning(workerStateEvent -> {
connectionService.setDelay(Duration.ZERO);
if(!ElectrumServer.isConnected()) {
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerDisplayName()));
}
});
connectionService.setOnSucceeded(successEvent -> {
@@ -285,26 +283,25 @@ public class AppServices {
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
connectionService.resetConnection();
connectionService.closeConnection();
if(failEvent.getSource().getException() instanceof ServerConfigException) {
connectionService.setRestartOnFailure(false);
}
if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) {
TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException();
if(failEvent.getSource().getException() instanceof TlsServerException tlsServerException && failEvent.getSource().getException().getCause() != null) {
connectionService.setRestartOnFailure(false);
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
File crtFile = Config.get().getElectrumServerCert();
if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
"\n\nChange the configured server certificate if you would like to proceed.");
} else {
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
if(crtFile != null) {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
if(crtFile.delete()) {
@@ -316,10 +313,19 @@ public class AppServices {
}
}
}
} else if(tlsServerException.getCause().getCause() instanceof UnknownCertificateExpiredException expiredException) {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " has expired. "
+ tlsServerException.getMessage() + "." +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
Storage.saveCertificate(tlsServerException.getServer().getHost(), expiredException.getCertificate());
Platform.runLater(() -> restartService(connectionService));
return;
}
}
}
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().requiresTor()) {
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().isAutoSwitchProxy() && Config.get().requiresTor()) {
Config.get().setUseProxy(false);
Platform.runLater(() -> restartService(torService));
return;
@@ -407,23 +413,6 @@ public class AppServices {
EventManager.get().post(new TorReadyStatusEvent());
});
torService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
if(exception instanceof TorServerAlreadyBoundException) {
String proxyServer = Config.get().getProxyServer();
if(proxyServer == null || proxyServer.equals("")) {
proxyServer = "localhost:9050";
Config.get().setProxyServer(proxyServer);
}
if(proxyServer.equals("localhost:9050") || proxyServer.equals("127.0.0.1:9050")) {
Config.get().setUseProxy(true);
torService.cancel();
restartServices();
EventManager.get().post(new TorExternalStatusEvent());
return;
}
}
EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException()));
});
@@ -483,8 +472,7 @@ public class AppServices {
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
} else if(AppServices.isTorRunning()) {
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
proxy = Tor.getDefault().getProxy();
}
//Setting new proxy authentication credentials will force a new Tor circuit to be created
@@ -499,8 +487,12 @@ public class AppServices {
return proxy;
}
static void initialize(MainApp application) {
INSTANCE = new AppServices(application);
public static void initialize(Application application) {
INSTANCE = new AppServices(application, new DefaultInteractionServices());
}
public static void initialize(Application application, InteractionServices interactionServices) {
INSTANCE = new AppServices(application, interactionServices);
}
public static AppServices get() {
@@ -515,6 +507,10 @@ public class AppServices {
return get().sorobanServices;
}
public static InteractionServices getInteractionServices() {
return get().interactionServices;
}
public static PayNymService getPayNymService() {
if(payNymService == null) {
HostAndPort torProxy = getTorProxy();
@@ -531,7 +527,7 @@ public class AppServices {
public static HostAndPort getTorProxy() {
return AppServices.isTorRunning() ?
HostAndPort.fromParts("localhost", TorService.PROXY_PORT) :
Tor.getDefault().getProxyHostAndPort() :
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
}
@@ -561,7 +557,7 @@ public class AppServices {
private static Image getWindowIcon() {
if(windowIcon == null) {
windowIcon = new Image(MainApp.class.getResourceAsStream("/image/sparrow-icon.png"));
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
}
return windowIcon;
@@ -571,7 +567,7 @@ public class AppServices {
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
}
public MainApp getApplication() {
public Application getApplication() {
return application;
}
@@ -746,40 +742,7 @@ public class AppServices {
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
Alert alert = new Alert(alertType, content, buttons);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
if(graphic != null) {
alert.setGraphic(graphic);
}
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
Matcher matcher = linkPattern.matcher(content);
if(matcher.find()) {
String link = matcher.group(1);
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
hyperlinkLabel.getStyleClass().add("content");
Label label = new Label();
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
hyperlinkLabel.setOnAction(event -> {
alert.close();
get().getApplication().getHostServices().showDocument(link);
});
alert.getDialogPane().setContent(hyperlinkLabel);
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}
moveToActiveWindowScreen(alert);
return alert.showAndWait();
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
}
public static void setStageIcon(Window window) {
@@ -829,6 +792,24 @@ public class AppServices {
}
}
public static void openBlockExplorer(String txid) {
if(Config.get().isBlockExplorerDisabled()) {
return;
}
Server blockExplorer = Config.get().getBlockExplorer() == null ? BlockExplorer.MEMPOOL_SPACE.getServer() : Config.get().getBlockExplorer();
String url = blockExplorer.getUrl();
if(url.contains("{0}")) {
url = url.replace("{0}", txid);
} else {
if(Network.get() != Network.MAINNET) {
url += "/" + Network.get().getName();
}
url += "/tx/" + txid;
}
AppServices.get().getApplication().getHostServices().showDocument(url);
}
static void parseFileUriArguments(List<String> fileUriArguments) {
for(String fileUri : fileUriArguments) {
try {
@@ -1130,7 +1111,7 @@ public class AppServices {
Wallet wallet = walletTabData.getWallet();
Storage storage = walletTabData.getStorage();
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB) || CardApi.isReaderAvailable())) {
usbWallet = true;
if(deviceEnumerateService == null) {
@@ -1157,12 +1138,20 @@ public class AppServices {
@Subscribe
public void requestConnect(RequestConnectEvent event) {
onlineProperty.set(true);
if(Config.get().hasServer()) {
onlineProperty.set(true);
}
}
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
onlineProperty.set(false);
//Ensure services don't try to reconnect later
Platform.runLater(() -> {
connectionService.cancel();
ratesService.cancel();
versionCheckService.cancel();
});
}
@Subscribe
@@ -1209,7 +1198,7 @@ public class AppServices {
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
onlineProperty.set(false);
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
@@ -17,6 +17,12 @@ public class Args {
@Parameter(names = { "--level", "-l" }, description = "Set log level")
public Level level;
@Parameter(names = { "--terminal", "-t" }, description = "Terminal mode", arity = 0)
public boolean terminal;
@Parameter(names = { "--version", "-v" }, description = "Show version", arity = 0)
public boolean version;
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
public boolean help;
@@ -25,7 +25,7 @@ public abstract class BaseController {
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
if(position.getMajor() % 2 == 0) {
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
if(!hoverChunk.isOpCode()) {
Point2D pos = e.getScreenPosition();
@@ -0,0 +1,65 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
import com.sparrowwallet.sparrow.control.TextUtils;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.text.Font;
import org.controlsfx.control.HyperlinkLabel;
import java.util.Arrays;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.sparrowwallet.sparrow.AppServices.moveToActiveWindowScreen;
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
public class DefaultInteractionServices implements InteractionServices {
@Override
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
Alert alert = new Alert(alertType, content, buttons);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
if(graphic != null) {
alert.setGraphic(graphic);
}
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
Matcher matcher = linkPattern.matcher(content);
if(matcher.find()) {
String link = matcher.group(1);
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
hyperlinkLabel.getStyleClass().add("content");
Label label = new Label();
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
hyperlinkLabel.setOnAction(event -> {
alert.close();
AppServices.get().getApplication().getHostServices().showDocument(link);
});
alert.getDialogPane().setContent(hyperlinkLabel);
}
String[] lines = content.split("\r\n|\r|\n");
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}
moveToActiveWindowScreen(alert);
return alert.showAndWait();
}
@Override
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(walletName, keystore);
return passphraseDialog.showAndWait();
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.wallet.Keystore;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import java.util.Optional;
public interface InteractionServices {
Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons);
Optional<String> requestPassphrase(String walletName, Keystore keystore);
}
@@ -0,0 +1,34 @@
package com.sparrowwallet.sparrow;
public enum Interface {
DESKTOP, TERMINAL, SERVER;
private static Interface currentInterface;
public static Interface get() {
if(currentInterface == null) {
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
if(headless || monocle) {
currentInterface = TERMINAL;
if(headless && !monocle) {
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
}
} else {
currentInterface = DESKTOP;
}
}
return currentInterface;
}
public static void set(Interface interf) {
if(currentInterface != null && interf != currentInterface) {
throw new IllegalStateException("Interface already set to " + currentInterface);
}
currentInterface = interf;
}
}
@@ -1,9 +1,8 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.WalletIcon;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
@@ -12,40 +11,28 @@ import com.sparrowwallet.sparrow.net.PublicElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.instance.InstanceException;
import com.sparrowwallet.sparrow.instance.InstanceList;
import javafx.application.Application;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.6.6";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
public class SparrowDesktop extends Application {
private Stage mainStage;
private static SparrowInstance sparrowInstance;
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
LoggerFactory.getLogger(SparrowWallet.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
} else {
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
LoggerFactory.getLogger(SparrowWallet.class).error("Exception in thread \"" + t.getName() + "\"", e);
}
});
super.init();
@@ -58,6 +45,7 @@ public class MainApp extends Application {
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
AppServices.initialize(this);
@@ -77,7 +65,7 @@ public class MainApp extends Application {
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getServer());
}
}
}
@@ -129,102 +117,9 @@ public class MainApp extends Application {
Config.get().setAppWidth(mainStage.getWidth());
Config.get().setAppHeight(mainStage.getHeight());
mainStage.close();
if(sparrowInstance != null) {
sparrowInstance.freeLock();
}
}
public static void main(String[] argv) {
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);
if(args.help) {
jCommander.usage();
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
}
if(args.dir != null) {
System.setProperty(APP_HOME_PROPERTY, args.dir);
getLogger().info("Using configured Sparrow home folder of " + args.dir);
}
if(args.network != null) {
Network.set(args.network);
} else {
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
if(envNetwork != null) {
try {
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
} catch(Exception e) {
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
}
}
}
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
if(testnetFlag.exists()) {
Network.set(Network.TESTNET);
}
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);
}
if(Network.get() != Network.MAINNET) {
getLogger().info("Using " + Network.get() + " configuration");
}
List<String> fileUriArguments = jCommander.getUnknownOptions();
try {
sparrowInstance = new SparrowInstance(fileUriArguments);
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
} catch(InstanceException e) {
getLogger().error("Could not access application lock", e);
}
if(!fileUriArguments.isEmpty()) {
AppServices.parseFileUriArguments(fileUriArguments);
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
}
private static Logger getLogger() {
return LoggerFactory.getLogger(MainApp.class);
}
private static class SparrowInstance extends InstanceList {
private final List<String> fileUriArguments;
public SparrowInstance(List<String> fileUriArguments) {
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
this.fileUriArguments = fileUriArguments;
}
@Override
protected void receiveMessageList(List<String> messageList) {
if(messageList != null && !messageList.isEmpty()) {
AppServices.parseFileUriArguments(messageList);
AppServices.openFileUriArguments(null);
}
}
@Override
protected List<String> sendMessageList() {
return fileUriArguments;
}
@Override
protected void beforeExit() {
getLogger().info("Opening files/URIs in already running instance, exiting...");
SparrowWallet.Instance instance = SparrowWallet.getSparrowInstance();
if(instance != null) {
instance.freeLock();
}
}
}
@@ -0,0 +1,155 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.instance.InstanceException;
import com.sparrowwallet.sparrow.instance.InstanceList;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sun.javafx.application.PlatformImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.File;
import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.7.4";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
private static Instance instance;
public static void main(String[] argv) {
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);
if(args.help) {
jCommander.usage();
System.exit(0);
}
if(args.version) {
System.out.println("Sparrow Wallet " + APP_VERSION);
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
}
if(args.dir != null) {
System.setProperty(APP_HOME_PROPERTY, args.dir);
getLogger().info("Using configured Sparrow home folder of " + args.dir);
}
if(args.network != null) {
Network.set(args.network);
} else {
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
if(envNetwork != null) {
try {
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
} catch(Exception e) {
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
}
}
}
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
if(testnetFlag.exists()) {
Network.set(Network.TESTNET);
}
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);
}
if(Network.get() != Network.MAINNET) {
getLogger().info("Using " + Network.get() + " configuration");
}
List<String> fileUriArguments = jCommander.getUnknownOptions();
try {
instance = new Instance(fileUriArguments);
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
} catch(InstanceException e) {
getLogger().error("Could not access application lock", e);
}
if(!fileUriArguments.isEmpty()) {
AppServices.parseFileUriArguments(fileUriArguments);
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
if(args.terminal) {
Interface.set(Interface.TERMINAL);
}
try {
if(Interface.get() == Interface.TERMINAL) {
PlatformImpl.setTaskbarApplication(false);
Drongo.removeRootLogAppender("STDOUT");
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowTerminal.class, SparrowWalletPreloader.class, argv);
} else {
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowDesktop.class, SparrowWalletPreloader.class, argv);
}
} catch(UnsupportedOperationException e) {
Drongo.removeRootLogAppender("STDOUT");
getLogger().error("Unable to launch application", e);
System.out.println("No display detected. Use Sparrow Server on a headless (no display) system.");
try {
if(instance != null) {
instance.freeLock();
}
} catch(InstanceException instanceException) {
getLogger().error("Unable to free instance lock", e);
}
}
}
public static Instance getSparrowInstance() {
return instance;
}
private static Logger getLogger() {
return LoggerFactory.getLogger(SparrowWallet.class);
}
public static class Instance extends InstanceList {
private final List<String> fileUriArguments;
public Instance(List<String> fileUriArguments) {
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
this.fileUriArguments = fileUriArguments;
}
@Override
protected void receiveMessageList(List<String> messageList) {
if(messageList != null && !messageList.isEmpty()) {
AppServices.parseFileUriArguments(messageList);
AppServices.openFileUriArguments(null);
}
}
@Override
protected List<String> sendMessageList() {
return fileUriArguments;
}
@Override
protected void beforeExit() {
getLogger().info("Opening files/URIs in already running instance, exiting...");
}
}
}
@@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow;
import javafx.application.Preloader;
import javafx.stage.Stage;
public class MainAppPreloader extends Preloader {
public class SparrowWalletPreloader extends Preloader {
@Override
public void start(Stage stage) {
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
@@ -1,17 +0,0 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,104 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public enum UnitFormat {
DOT {
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
public DecimalFormat getBtcFormat() {
btcFormat.setMaximumFractionDigits(8);
return btcFormat;
}
public DecimalFormat getSatsFormat() {
return satsFormat;
}
public DecimalFormat getTableBtcFormat() {
return tableBtcFormat;
}
public DecimalFormat getCurrencyFormat() {
return currencyFormat;
}
public DecimalFormatSymbols getDecimalFormatSymbols() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.');
symbols.setGroupingSeparator(',');
return symbols;
}
},
COMMA {
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
public DecimalFormat getBtcFormat() {
btcFormat.setMaximumFractionDigits(8);
return btcFormat;
}
public DecimalFormat getSatsFormat() {
return satsFormat;
}
public DecimalFormat getTableBtcFormat() {
return tableBtcFormat;
}
public DecimalFormat getCurrencyFormat() {
return currencyFormat;
}
public DecimalFormatSymbols getDecimalFormatSymbols() {
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator(',');
symbols.setGroupingSeparator('.');
return symbols;
}
};
public abstract DecimalFormatSymbols getDecimalFormatSymbols();
public abstract DecimalFormat getBtcFormat();
public abstract DecimalFormat getSatsFormat();
public abstract DecimalFormat getTableBtcFormat();
public abstract DecimalFormat getCurrencyFormat();
public String formatBtcValue(Long amount) {
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
}
public String tableFormatBtcValue(Long amount) {
return getTableBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
}
public String formatSatsValue(Long amount) {
return getSatsFormat().format(amount);
}
public String formatCurrencyValue(double amount) {
return getCurrencyFormat().format(amount);
}
public String getGroupingSeparator() {
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
}
public String getDecimalSeparator() {
return Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
}
}
@@ -40,7 +40,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()), false));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
@@ -20,7 +20,7 @@ import java.util.*;
public class AddressTreeTable extends CoinTreeTable {
public void initialize(NodeEntry rootEntry) {
getStyleClass().add("address-treetable");
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
String address = rootEntry.getAddress().toString();
updateAll(rootEntry);
@@ -114,7 +114,7 @@ public class AddressTreeTable extends CoinTreeTable {
}
public void updateAll(NodeEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.collect.Lists;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
@@ -35,8 +36,7 @@ public class BalanceChart extends LineChart<Number, Number> {
getData().add(balanceSeries);
update(walletTransactionsEntry);
BitcoinUnit unit = Config.get().getBitcoinUnit();
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
setUnitFormat(walletTransactionsEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
}
public void update(WalletTransactionsEntry walletTransactionsEntry) {
@@ -116,12 +116,16 @@ public class BalanceChart extends LineChart<Number, Number> {
}
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
if(format == null) {
format = UnitFormat.DOT;
}
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
}
@@ -0,0 +1,234 @@
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.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
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 javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class CardImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
private final KeystoreCardImport importer;
private final 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");
this.importer = importer;
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
}
@Override
protected Control createButton() {
importButton = new Button("Import");
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
tapGlyph.setFontSize(12);
importButton.setGraphic(tapGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importCard();
});
return importButton;
}
private void importCard() {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getPinEntry());
showHideLink.setVisible(false);
setExpanded(true);
importButton.setDisable(false);
return;
}
StringProperty messageProperty = new SimpleStringProperty();
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
try {
if(!importer.isInitialized()) {
setDescription("Card not initialized");
setContent(getInitializationPanel(messageProperty));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
} catch(CardException e) {
setError("Card Error", e.getMessage());
importButton.setDisable(false);
return;
}
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
});
cardImportService.setOnFailed(event -> {
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getPinEntry());
} else {
log.error("Error importing keystore from card", event.getSource().getException());
setError("Import Error", rootCause.getMessage());
}
importButton.setDisable(false);
});
cardImportService.start();
}
private Node getInitializationPanel(StringProperty messageProperty) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
automatic.setToggleGroup(toggleGroup);
advanced.setToggleGroup(toggleGroup);
automatic.setSelected(true);
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
entropy.setDisable(newValue == automatic);
});
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
setDescription("Leave card on reader");
setExpanded(false);
importButton.setDisable(false);
});
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getPinEntry());
importButton.setDisable(false);
} else {
log.error("Error initializing card", failEvent.getSource().getException());
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
initializeButton.setDisable(false);
}
});
cardInitializationService.start();
});
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
return contentBox;
}
private Node getPinEntry() {
VBox vBox = new VBox();
CustomPasswordField pinField = new ViewPasswordField();
pinField.setPromptText("PIN Code");
importButton.setDefaultButton(true);
pin.bind(pinField.textProperty());
HBox.setHgrow(pinField, Priority.ALWAYS);
Platform.runLater(pinField::requestFocus);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(pinField);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(50);
vBox.getChildren().add(contentBox);
return vBox;
}
public static class CardInitializationService extends Service<Void> {
private final KeystoreCardImport cardImport;
private final String pin;
private final byte[] chainCode;
private final StringProperty messageProperty;
public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) {
this.cardImport = cardImport;
this.pin = pin;
this.chainCode = chainCode;
this.messageProperty = messageProperty;
}
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
cardImport.initialize(pin, chainCode, messageProperty);
return null;
}
};
}
}
public static class CardImportService extends Service<Keystore> {
private final KeystoreCardImport cardImport;
private final String pin;
private final List<ChildNumber> derivation;
private final StringProperty messageProperty;
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
this.cardImport = cardImport;
this.pin = pin;
this.derivation = derivation;
this.messageProperty = messageProperty;
}
@Override
protected Task<Keystore> createTask() {
return new Task<>() {
@Override
protected Keystore call() throws Exception {
return cardImport.getKeystore(pin, derivation, messageProperty);
}
};
}
}
}
@@ -0,0 +1,93 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
private final CustomPasswordField existingPin;
private final CustomPasswordField newPin;
private final CustomPasswordField newPinConfirm;
private final CheckBox backupFirst;
private final ButtonType okButtonType;
public CardPinDialog() {
this.existingPin = new ViewPasswordField();
this.newPin = new ViewPasswordField();
this.newPinConfirm = new ViewPasswordField();
this.backupFirst = new CheckBox();
final DialogPane dialogPane = getDialogPane();
setTitle("Change Card PIN");
dialogPane.setHeaderText("Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(260);
AppServices.moveToActiveWindowScreen(this);
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lock.setFontSize(50);
dialogPane.setGraphic(lock);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
fieldset.setSpacing(10);
Field currentField = new Field();
currentField.setText("Current PIN:");
currentField.getInputs().add(existingPin);
Field newField = new Field();
newField.setText("New PIN:");
newField.getInputs().add(newPin);
Field confirmField = new Field();
confirmField.setText("Confirm new PIN:");
confirmField.getInputs().add(newPinConfirm);
Field backupField = new Field();
backupField.setText("Backup First:");
backupField.getInputs().add(backupFirst);
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < 6 || existingPin.getText().length() > 32));
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < 6 || newPin.getText().length() > 32));
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
});
okButtonType = new javafx.scene.control.ButtonType("Change", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
okButton.setPrefWidth(130);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < 6 || existingPin.getText().length() > 32
|| newPin.getText().length() < 6 || newPin.getText().length() > 32
|| !newPin.getText().equals(newPinConfirm.getText()),
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
okButton.disableProperty().bind(isInvalid);
Platform.runLater(existingPin::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? new CardPinChange(existingPin.getText(), newPin.getText(), backupFirst.isSelected()) : null);
}
public record CardPinChange(String currentPin, String newPin, boolean backupFirst) { }
}
@@ -1,28 +1,31 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.UnitFormat;
import javafx.scene.chart.NumberAxis;
import javafx.util.StringConverter;
import java.text.ParseException;
final class CoinAxisFormatter extends StringConverter<Number> {
private final UnitFormat unitFormat;
private final BitcoinUnit bitcoinUnit;
public CoinAxisFormatter(NumberAxis axis, BitcoinUnit unit) {
public CoinAxisFormatter(NumberAxis axis, UnitFormat format, BitcoinUnit unit) {
this.unitFormat = format;
this.bitcoinUnit = unit;
}
@Override
public String toString(Number object) {
Double value = bitcoinUnit.getValue(object.longValue());
return CoinTextFormatter.COIN_FORMAT.format(value);
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
}
@Override
public Number fromString(String string) {
try {
Number number = CoinTextFormatter.COIN_FORMAT.parse(string);
Number number = new CoinTextFormatter(unitFormat).getCoinFormat().parse(string);
return bitcoinUnit.getSatsValue(number.doubleValue());
} catch (ParseException e) {
throw new RuntimeException(e);
@@ -2,30 +2,35 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.control.*;
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;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
class CoinCell extends TreeTableCell<Entry, Number> {
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsListener {
private final CoinTooltip tooltip;
private final CoinContextMenu contextMenu;
private final Tooltip tooltip;
private IntegerProperty confirmationsProperty;
public CoinCell() {
super();
tooltip = new Tooltip();
tooltip = new CoinTooltip();
tooltip.setShowDelay(Duration.millis(500));
contextMenu = new CoinContextMenu();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
@@ -40,34 +45,32 @@ class CoinCell extends TreeTableCell<Entry, Number> {
setText(null);
setGraphic(null);
setTooltip(null);
setContextMenu(null);
} else {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
UnitFormat format = coinTreeTable.getUnitFormat();
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
String satsValue = format.formatSatsValue(amount.longValue());
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.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
tooltip.setText(btcValue + " " + BitcoinUnit.BTC.getLabel());
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
String tooltipValue = tooltip.getText();
contextMenu.updateAmount(amount);
setContextMenu(contextMenu);
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
});
if(entry instanceof TransactionEntry transactionEntry) {
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
if(transactionEntry.isConfirming()) {
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
@@ -97,4 +100,97 @@ class CoinCell extends TreeTableCell<Entry, Number> {
}
}
}
@Override
public IntegerProperty getConfirmationsProperty() {
if(confirmationsProperty == null) {
confirmationsProperty = new SimpleIntegerProperty();
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
getStyleClass().remove("confirming");
confirmationsProperty.unbind();
}
});
}
return confirmationsProperty;
}
private static final class CoinTooltip extends Tooltip {
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
private boolean showConfirmations;
private String value;
public void setValue(String value) {
this.value = value;
setTooltipText();
}
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
showConfirmations = true;
int confirmations = txEntryConfirmationsProperty.get();
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
confirmationsProperty.bind(txEntryConfirmationsProperty);
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
setTooltipText();
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
confirmationsProperty.unbind();
}
});
} else {
confirmationsProperty.unbind();
confirmationsProperty.set(confirmations);
}
setTooltipText();
}
private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
}
public String getConfirmationsDescription() {
int confirmations = confirmationsProperty.get();
if(confirmations == 0) {
return "Unconfirmed in mempool";
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
} else {
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
}
}
}
private static class CoinContextMenu extends ContextMenu {
private Number amount;
public void updateAmount(Number amount) {
if(amount.equals(this.amount)) {
return;
}
this.amount = amount;
getItems().clear();
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
copySatsValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(amount.toString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
content.putString(format.formatBtcValue(amount.longValue()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
}
}
}
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
@@ -12,13 +12,7 @@ import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class CoinLabel extends Label {
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
@@ -58,8 +52,9 @@ public class CoinLabel extends Label {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
String satsValue = format.formatSatsValue(value) + " sats";
String btcValue = format.formatBtcValue(value) + " BTC";
BitcoinUnit unit = bitcoinUnit;
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
@@ -89,16 +84,12 @@ public class CoinLabel extends Label {
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
content.putString(format.formatBtcValue(getValue()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
}
}
public static DecimalFormat getBTCFormat() {
BTC_FORMAT.setMaximumFractionDigits(8);
return BTC_FORMAT;
}
}
@@ -1,24 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.UnitFormat;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public CoinTextFormatter(UnitFormat unitFormat) {
super(new CoinFilter(unitFormat));
}
public CoinTextFormatter() {
super(new CoinFilter());
public UnitFormat getUnitFormat() {
return ((CoinFilter)getFilter()).unitFormat;
}
public DecimalFormat getCoinFormat() {
return ((CoinFilter)getFilter()).coinFormat;
}
private static class CoinFilter implements UnaryOperator<Change> {
private final UnitFormat unitFormat;
private final DecimalFormat coinFormat;
private final Pattern coinValidation;
public CoinFilter(UnitFormat unitFormat) {
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols());
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
}
@Override
public Change apply(Change change) {
String oldText = change.getControlText();
@@ -30,17 +45,17 @@ public class CoinTextFormatter extends TextFormatter<String> {
String noFractionCommaText = newText;
int commasRemoved = 0;
int dotIndex = newText.indexOf(".");
int dotIndex = newText.indexOf(unitFormat.getDecimalSeparator());
if(dotIndex > -1) {
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(",", "");
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
commasRemoved = newText.length() - noFractionCommaText.length();
}
if(!COIN_VALIDATION.matcher(noFractionCommaText).matches()) {
if(!coinValidation.matcher(noFractionCommaText).matches()) {
return null;
}
if(",".equals(change.getText())) {
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
return null;
}
@@ -48,20 +63,20 @@ public class CoinTextFormatter extends TextFormatter<String> {
return change;
}
if(change.isDeleted() && ",".equals(deleted) && change.getRangeStart() > 0) {
if(change.isDeleted() && unitFormat.getGroupingSeparator().equals(deleted) && change.getRangeStart() > 0) {
noFractionCommaText = noFractionCommaText.substring(0, change.getRangeStart() - 1) + noFractionCommaText.substring(change.getRangeEnd() - 1);
}
try {
Number value = COIN_FORMAT.parse(noFractionCommaText);
String correct = COIN_FORMAT.format(value.doubleValue());
Number value = coinFormat.parse(noFractionCommaText);
String correct = coinFormat.format(value.doubleValue());
String compare = newText;
if(compare.contains(".") && compare.endsWith("0")) {
if(compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) {
compare = compare.replaceAll("0*$", "");
}
if(compare.endsWith(".")) {
if(compare.endsWith(unitFormat.getDecimalSeparator())) {
compare = compare.substring(0, compare.length() - 1);
}
@@ -79,11 +94,11 @@ public class CoinTextFormatter extends TextFormatter<String> {
if(correct.length() != newText.length()) {
String postCorrect = correct.substring(Math.min(change.getCaretPosition(), correct.length()));
int commasAfter = postCorrect.length() - postCorrect.replace(",", "").length();
int caretShift = change.isDeleted() && ".".equals(deleted) ? commasAfter : 0;
int commasAfter = postCorrect.length() - postCorrect.replace(unitFormat.getGroupingSeparator(), "").length();
int caretShift = change.isDeleted() && unitFormat.getDecimalSeparator().equals(deleted) ? commasAfter : 0;
int caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
if(caret >= 0) {
if(caret >= 0 && caret <= change.getControlNewText().length()) {
change.setCaretPosition(caret);
change.setAnchor(caret);
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
@@ -26,25 +27,36 @@ import java.util.Optional;
public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat;
public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit;
}
public void setBitcoinUnit(BitcoinUnit bitcoinUnit) {
this.bitcoinUnit = bitcoinUnit;
public UnitFormat getUnitFormat() {
return unitFormat;
}
public void setBitcoinUnit(Wallet wallet) {
setBitcoinUnit(wallet, Config.get().getBitcoinUnit());
public void setUnitFormat(Wallet wallet) {
setUnitFormat(wallet, Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
public void setUnitFormat(Wallet wallet, UnitFormat format) {
setUnitFormat(wallet, format, Config.get().getBitcoinUnit());
}
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
if(format == null) {
format = UnitFormat.DOT;
}
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
boolean changed = (bitcoinUnit != unit);
boolean changed = (unitFormat != format);
changed |= (bitcoinUnit != unit);
this.unitFormat = format;
this.bitcoinUnit = unit;
if(changed && !getChildren().isEmpty()) {
@@ -81,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
@@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.property.IntegerProperty;
public interface ConfirmationsListener {
IntegerProperty getConfirmationsProperty();
}
@@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
@@ -11,8 +11,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.util.Locale;
public class CopyableCoinLabel extends CopyableLabel {
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
@@ -24,7 +22,7 @@ public class CopyableCoinLabel extends CopyableLabel {
public CopyableCoinLabel(String text) {
super(text);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
@@ -42,19 +40,23 @@ public class CopyableCoinLabel extends CopyableLabel {
}
public void refresh() {
refresh(Config.get().getBitcoinUnit());
refresh(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
}
public void refresh(BitcoinUnit bitcoinUnit) {
setValueAsText(getValue(), bitcoinUnit);
public void refresh(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
setValueAsText(getValue(), unitFormat, bitcoinUnit);
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
if(unitFormat == null) {
unitFormat = UnitFormat.DOT;
}
String satsValue = unitFormat.formatSatsValue(value) + " sats";
String btcValue = unitFormat.formatBtcValue(value) + " BTC";
BitcoinUnit unit = bitcoinUnit;
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
@@ -84,7 +86,8 @@ public class CopyableCoinLabel extends CopyableLabel {
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
content.putString(format.formatBtcValue(getValue()));
Clipboard.getSystemClipboard().setContent(content);
});
@@ -1,28 +1,13 @@
package com.sparrowwallet.sparrow.control;
import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfWriter;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import com.sparrowwallet.sparrow.AppServices;
import javafx.embed.swing.SwingFXUtils;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.PdfUtils;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
super(ur);
@@ -31,43 +16,11 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
dialogPane.getButtonTypes().add(pdfButtonType);
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
pdfButton.setGraphicTextGap(5);
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
savePdf(walletName, outputDescriptor, ur);
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
event.consume();
});
}
private void savePdf(String walletName, String outputDescriptor, UR ur) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PDF");
fileChooser.setInitialFileName(walletName + ".pdf");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(Document document = new Document()) {
document.setMargins(36, 36, 48, 36);
PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
document.add(title);
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
String fragment = urEncoder.nextPart();
if(urEncoder.isSinglePart()) {
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
document.add(image);
}
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
document.add(descriptor);
} catch(Exception e) {
log.error("Error creating descriptor PDF", e);
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
}
}
}
}
@@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppServices.getDevices();
List<Device> devices = getDevices();
if(devices == null || devices.isEmpty()) {
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
@@ -96,6 +96,10 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
}
protected List<Device> getDevices() {
return AppServices.getDevices();
}
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
@@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> {
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet;
this.outputDescriptor = outputDescriptor;
@@ -0,0 +1,29 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceAddressEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceGetAddressDialog extends DeviceDialog<Address> {
public DeviceGetAddressDialog(List<String> operationFingerprints) {
super(operationFingerprints);
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice);
}
@Subscribe
public void deviceAddress(DeviceAddressEvent event) {
setResult(event.getAddress());
}
}
@@ -1,25 +1,33 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy;
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.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
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.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -34,8 +42,10 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
@@ -58,8 +68,12 @@ public class DevicePane extends TitledDescriptionPane {
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private ButtonBase getPrivateKeyButton;
private Button getAddressButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty("");
private final StringProperty messageProperty = new SimpleStringProperty("");
private boolean defaultDevice;
@@ -83,13 +97,17 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.wallet = wallet;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
@@ -106,6 +124,10 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
@@ -152,6 +174,10 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
@@ -178,6 +204,41 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
}
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = deviceOperation;
this.wallet = null;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
this.availableAccounts = null;
setDefaultStatus();
showHideLink.setVisible(false);
ButtonBase button;
if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) {
createGetPrivateKeyButton();
button = getPrivateKeyButton;
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
createGetAddressButton();
button = getAddressButton;
} else {
throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation);
}
initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().add(button);
}
private void initialise(Device device) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
@@ -201,7 +262,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private void setDefaultStatus() {
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked");
}
private void createUnlockButton() {
@@ -321,6 +382,60 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setVisible(false);
}
private void createGetPrivateKeyButton() {
int currentSlot = 0;
boolean initialized = true;
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), null);
currentSlot = cardApi.getCurrentSlot();
initialized = cardApi.isInitialized();
} catch(Exception e) {
//ignore
}
getPrivateKeyButton = currentSlot > 0 ? new SplitMenuButton() : new Button();
getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
getPrivateKeyButton.setText("Get Private Key");
getPrivateKeyButton.setOnAction(event -> {
getPrivateKeyButton.setDisable(true);
getPrivateKey(null);
});
if(getPrivateKeyButton instanceof SplitMenuButton getPrivateKeyMenuButton) {
int[] previousSlots = IntStream.range(0, currentSlot).toArray();
for(int previousSlot : previousSlots) {
MenuItem previousSlotItem = new MenuItem("Slot #" + previousSlot);
previousSlotItem.setOnAction(event -> {
getPrivateKeyButton.setDisable(true);
getPrivateKey(previousSlot);
});
getPrivateKeyMenuButton.getItems().add(previousSlotItem);
}
if(initialized) {
int finalSlot = currentSlot;
MenuItem currentSlotItem = new MenuItem("Current Slot");
currentSlotItem.setOnAction(event -> {
getPrivateKeyButton.setDisable(true);
getPrivateKey(finalSlot);
});
getPrivateKeyMenuButton.getItems().add(currentSlotItem);
}
}
getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
getPrivateKeyButton.setVisible(false);
}
private void createGetAddressButton() {
getAddressButton = new Button("Get Address");
getAddressButton.setAlignment(Pos.CENTER_RIGHT);
getAddressButton.setOnAction(event -> {
getAddressButton.setDisable(true);
getAddress();
});
getAddressButton.managedProperty().bind(getAddressButton.visibleProperty());
getAddressButton.setVisible(false);
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@@ -554,7 +669,36 @@ public class DevicePane extends TitledDescriptionPane {
}
private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
if(!cardApi.isInitialized()) {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(importButton));
showHideLink.setVisible(false);
setExpanded(true);
importButton.setDisable(false);
return;
}
setDescription("Card not initialized");
setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
} catch(Exception e) {
log.error("Import Error: " + e.getMessage(), e);
setError("Import Error", e.getMessage());
importButton.setDisable(false);
}
} else if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
@@ -591,18 +735,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
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);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
importKeystore(derivation, keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
@@ -616,20 +749,74 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.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);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
private void sign() {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
handleCardOperation(signService, signButton, "Signing", true, event -> {
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
});
} catch(Exception e) {
log.error("Signing Error: " + e.getMessage(), e);
setError("Signing Error", e.getMessage());
signButton.setDisable(false);
}
} else {
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
signPSBTService.setOnSucceeded(workerStateEvent -> {
PSBT signedPsbt = signPSBTService.getValue();
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
signButton.setDisable(false);
});
setDescription("Signing...");
showHideLink.setVisible(false);
signPSBTService.start();
}
}
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
if(pinRequired && pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(operationButton));
showHideLink.setVisible(false);
setExpanded(true);
operationButton.setDisable(false);
return;
}
service.setOnSucceeded(successHandler);
service.setOnFailed(event -> {
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
} else {
log.error(operationDescription + " Error: " + rootCause.getMessage(), event.getSource().getException());
setError(operationDescription + " Error", rootCause.getMessage());
}
operationButton.setDisable(false);
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
signButton.setDisable(false);
});
setDescription("Signing...");
showHideLink.setVisible(false);
signPSBTService.start();
service.start();
}
private void displayAddress() {
@@ -647,17 +834,32 @@ public class DevicePane extends TitledDescriptionPane {
}
private void signMessage() {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
signMessageService.setOnFailed(failedEvent -> {
setError("Could not sign message", signMessageService.getException().getMessage());
signMessageButton.setDisable(false);
});
setDescription("Signing message...");
signMessageService.start();
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
} catch(Exception e) {
log.error("Signing Error: " + e.getMessage(), e);
setError("Signing Error", e.getMessage());
signButton.setDisable(false);
}
} else {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
signMessageService.setOnFailed(failedEvent -> {
setError("Could not sign message", signMessageService.getException().getMessage());
signMessageButton.setDisable(false);
});
setDescription("Signing message...");
signMessageService.start();
}
}
private void discoverKeystores() {
@@ -717,6 +919,55 @@ public class DevicePane extends TitledDescriptionPane {
getXpubsService.start();
}
private void getPrivateKey(Integer slot) {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(slot, messageProperty);
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
});
} catch(Exception e) {
log.error("Private Key Error: " + e.getMessage(), e);
setError("Private Key Error", e.getMessage());
getPrivateKeyButton.setDisable(false);
}
}
}
private void getAddress() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
if(!cardApi.isInitialized()) {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(getAddressButton));
showHideLink.setVisible(false);
setExpanded(true);
getAddressButton.setDisable(false);
return;
}
setDescription("Card not initialized");
setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
Service<Address> addressService = cardApi.getAddressService(messageProperty);
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
});
} catch(Exception e) {
log.error("Address Error: " + e.getMessage(), e);
setError("Address Error", e.getMessage());
getAddressButton.setDisable(false);
}
}
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
@@ -724,7 +975,7 @@ public class DevicePane extends TitledDescriptionPane {
}
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
showHideLink.setVisible(!device.isCard());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
@@ -743,6 +994,16 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
if(defaultDevice) {
getPrivateKeyButton.getStyleClass().add("default-button");
}
getPrivateKeyButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
getAddressButton.setDefaultButton(defaultDevice);
getAddressButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@@ -750,7 +1011,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
derivationField.setDisable(device.isCard() || keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@@ -785,11 +1046,92 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox;
}
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
automatic.setToggleGroup(toggleGroup);
advanced.setToggleGroup(toggleGroup);
automatic.setSelected(true);
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
entropy.setDisable(newValue == automatic);
});
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
if(deviceOperation == DeviceOperation.IMPORT) {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
}
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
});
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
operationButton.setDisable(false);
} else {
log.error("Error initializing card", failEvent.getSource().getException());
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
initializeButton.setDisable(false);
}
});
cardInitializationService.start();
});
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
return contentBox;
}
private Node getCardPinEntry(ButtonBase operationButton) {
VBox vBox = new VBox();
CustomPasswordField pinField = new ViewPasswordField();
pinField.setPromptText("PIN Code");
if(operationButton instanceof Button defaultButton) {
defaultButton.setDefaultButton(true);
}
pin.bind(pinField.textProperty());
HBox.setHgrow(pinField, Priority.ALWAYS);
Platform.runLater(pinField::requestFocus);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(pinField);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(50);
vBox.getChildren().add(contentBox);
return vBox;
}
public Device getDevice() {
return device;
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
}
}
@@ -2,17 +2,24 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class DeviceSignDialog extends DeviceDialog<PSBT> {
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
private final Wallet wallet;
private final PSBT psbt;
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
public DeviceSignDialog(Wallet wallet, List<String> operationFingerprints, PSBT psbt) {
super(operationFingerprints);
this.wallet = wallet;
this.psbt = psbt;
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -23,7 +30,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(psbt, device, defaultDevice);
return new DevicePane(wallet, psbt, device, defaultDevice);
}
@Subscribe
@@ -0,0 +1,32 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
public DeviceUnsealDialog(List<String> operationFingerprints) {
super(operationFingerprints);
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
}
@Subscribe
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
}
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
}
@@ -9,9 +9,11 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
@@ -30,7 +32,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class EntryCell extends TreeTableCell<Entry, Entry> {
public class EntryCell extends TreeTableCell<Entry, Entry> implements ConfirmationsListener {
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
@@ -38,6 +40,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
private static EntryCell lastCell;
private IntegerProperty confirmationsProperty;
public EntryCell() {
super();
setAlignment(Pos.CENTER_LEFT);
@@ -96,11 +100,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
increaseFeeButton.setOnAction(event -> {
increaseFee(transactionEntry);
increaseFee(transactionEntry, false);
});
actionBox.getChildren().add(increaseFeeButton);
}
@@ -119,7 +124,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(nodeEntry.getNode().toString());
@@ -188,7 +193,22 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
}
private static void increaseFee(TransactionEntry transactionEntry) {
@Override
public IntegerProperty getConfirmationsProperty() {
if(confirmationsProperty == null) {
confirmationsProperty = new SimpleIntegerProperty();
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
getStyleClass().remove("confirming");
confirmationsProperty.unbind();
}
});
}
return confirmationsProperty;
}
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
@@ -200,6 +220,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
if(utxos.isEmpty()) {
log.error("No UTXOs to replace");
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction - no replaceable UTXOs were found.");
return;
}
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
@@ -222,7 +248,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
//Remove any UTXOs created by the transaction that is to be replaced
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
Collections.shuffle(walletUtxos);
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
//If there is insufficient change output, include another random UTXO so the fee can be increased
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
utxos.add(utxo);
@@ -279,6 +305,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
return;
}
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);
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, blockTransaction.getFee(), true)));
}
@@ -318,7 +353,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().get(0).getWalletModel().isCard()) &&
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
@@ -410,6 +445,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
return increaseFeeGlyph;
}
private static Glyph getCancelTransactionRBFGlyph() {
Glyph cancelTxGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BAN);
cancelTxGlyph.setFontSize(12);
return cancelTxGlyph;
}
private static Glyph getIncreaseFeeCPFPGlyph() {
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
cpfpGlyph.setFontSize(12);
@@ -463,17 +504,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().add(viewTransaction);
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
hide();
increaseFee(transactionEntry);
increaseFee(transactionEntry, false);
});
getItems().add(increaseFee);
}
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> {
hide();
increaseFee(transactionEntry, true);
});
getItems().add(cancelTx);
}
if(containsWalletOutputs(transactionEntry)) {
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
@@ -485,6 +537,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
getItems().add(createCpfp);
}
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
getItems().add(openBlockExplorer);
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
@@ -506,6 +565,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
@@ -530,12 +595,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
}
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems) {
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
@@ -558,6 +623,42 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
getItems().add(signVerifyMessage);
}
if(addUtxoItems && nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
spendUtxos.setGraphic(getSendGlyph());
spendUtxos.setOnAction(AE -> {
hide();
EventManager.get().post(new SendActionEvent(nodeEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(nodeEntry.getWallet(), utxos)));
});
getItems().add(spendUtxos);
List<BlockTransactionHashIndex> unfrozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
if(!unfrozenUtxos.isEmpty()) {
MenuItem freezeUtxos = new MenuItem("Freeze UTXOs");
freezeUtxos.setGraphic(getFreezeGlyph());
freezeUtxos.setOnAction(AE -> {
hide();
unfrozenUtxos.forEach(utxo -> utxo.setStatus(Status.FROZEN));
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), unfrozenUtxos));
});
getItems().add(freezeUtxos);
}
List<BlockTransactionHashIndex> frozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
if(!frozenUtxos.isEmpty()) {
MenuItem unfreezeUtxos = new MenuItem("Unfreeze UTXOs");
unfreezeUtxos.setGraphic(getUnfreezeGlyph());
unfreezeUtxos.setOnAction(AE -> {
hide();
frozenUtxos.forEach(utxo -> utxo.setStatus(null));
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), frozenUtxos));
});
getItems().add(unfreezeUtxos);
}
}
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
@@ -566,14 +667,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
@@ -582,7 +675,19 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
getItems().addAll(copyAddress, copyOutputDescriptor);
if(nodeEntry != null) {
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
Script outputScript = nodeEntry.getWallet().getOutputScript(nodeEntry.getNode());
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(outputScript.getProgram()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHex);
}
}
}
@@ -653,13 +758,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
if(entry instanceof TransactionEntry) {
cell.getStyleClass().add("transaction-row");
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
if(!transactionEntry.isConfirming()) {
cell.getStyleClass().remove("confirming");
}
});
if(cell instanceof ConfirmationsListener confirmationsListener) {
if(transactionEntry.isConfirming()) {
cell.getStyleClass().add("confirming");
confirmationsListener.getConfirmationsProperty().bind(transactionEntry.confirmationsProperty());
} else {
confirmationsListener.getConfirmationsProperty().unbind();
}
}
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
@@ -1,7 +1,9 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.*;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
@@ -10,14 +12,9 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Currency;
import java.util.Locale;
public class FiatLabel extends CopyableLabel {
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
@@ -30,9 +27,9 @@ public class FiatLabel extends CopyableLabel {
public FiatLabel(String text) {
super(text);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat()));
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
tooltip = new Tooltip();
contextMenu = new FiatContextMenu();
}
@@ -83,14 +80,22 @@ public class FiatLabel extends CopyableLabel {
setCurrency(currency);
}
private void setValueAsText(long balance) {
public void refresh() {
refresh(Config.get().getUnitFormat());
}
public void refresh(UnitFormat unitFormat) {
setValueAsText(getValue(), unitFormat);
}
private void setValueAsText(long balance, UnitFormat unitFormat) {
if(getCurrency() != null && getBtcRate() > 0.0) {
BigDecimal satsBalance = BigDecimal.valueOf(balance);
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
String label = getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(getBtcRate()));
String label = getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(fiatBalance.doubleValue());
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(getBtcRate()));
setText(label);
setTooltip(tooltip);
@@ -162,8 +162,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
setError("Import Error", e.getMessage());
}
} else if(result.outputDescriptor != null) {
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
try {
wallets = List.of(result.outputDescriptor.toWallet());
importFile(importer.getName(), null, null);
} catch(ImportException e) {
log.error("Error importing QR", e);
@@ -193,6 +193,10 @@ public abstract class FileImportPane extends TitledDescriptionPane {
}
}
protected List<Wallet> getScannedWallets() {
return wallets;
}
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
if(wallets != null) {
for(Wallet wallet : wallets) {
@@ -215,7 +219,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password");
passwordField.setPromptText("Password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
@@ -0,0 +1,168 @@
package com.sparrowwallet.sparrow.control;
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.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
public class FileKeystoreExportPane extends TitledDescriptionPane {
private final Keystore keystore;
private final KeystoreFileExport exporter;
private final boolean scannable;
private final boolean file;
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.keystore = keystore;
this.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable();
this.file = exporter.isKeystoreExportFile();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
if(scannable && file) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else if(scannable) {
Button showButton = new Button("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
exportQR();
});
return showButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportQR() {
exportKeystore(null, keystore);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(keystore);
String fileName = keystore.getLabel();
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
exportKeystore(file, keystore);
}
}
private void exportKeystore(File file, Keystore exportKeystore) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.exportKeystore(exportKeystore, baos);
if(exporter.requiresSignature()) {
String message = baos.toString(StandardCharsets.UTF_8);
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
TextAreaDialog dialog = new TextAreaDialog(message, false);
dialog.setTitle("Sign " + exporter.getName() + " Export");
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
dialog.showAndWait();
Wallet wallet = new Wallet();
wallet.setScriptType(ScriptType.P2PKH);
wallet.getKeystores().add(keystore);
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
exporter.addSignature(keystore, optSignature.get(), baos);
}
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
exporter.addSignature(keystore, signature, baos);
} else {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
throw new RuntimeException("Export aborted due to lack of device message signing support.");
}
}
}
if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(baos.toByteArray());
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
}
} else {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
}
}
}
@@ -34,17 +34,4 @@ public class FileKeystoreImportPane extends FileImportPane {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
if(wallet == null || requiredDerivation == null) {
return 0;
}
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
if(account < 0) {
account = 0;
}
return account;
}
}
@@ -28,12 +28,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
private final boolean scannable;
private final boolean file;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
this.file = exporter.isWalletExportFile();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
@@ -41,7 +43,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
@Override
protected Control createButton() {
if(scannable) {
if(scannable && file) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
@@ -61,6 +63,15 @@ public class FileWalletExportPane extends TitledDescriptionPane {
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else if(scannable) {
Button showButton = new Button("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
exportQR();
});
return showButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
@@ -81,9 +92,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(wallet);
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
if(exporter instanceof Sparrow) {
fileName = wallet.getMasterName();
String walletModel = exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
String postfix = walletModel.equals(extension) ? "" : "-" + walletModel;
String fileName = wallet.getFullName() + postfix;
if(exporter.exportsAllWallets()) {
fileName = wallet.getMasterName() + postfix;
}
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
@@ -137,7 +150,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
@@ -18,7 +18,13 @@ public class FileWalletImportPane extends FileImportPane {
@Override
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importer.importWallet(inputStream, password);
Wallet wallet;
if(getScannedWallets() != null && !getScannedWallets().isEmpty()) {
wallet = getScannedWallets().iterator().next();
} else {
wallet = importer.importWallet(inputStream, password);
}
if(wallet.getName() == null) {
wallet.setName(fileName);
}
@@ -0,0 +1,66 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.Comparator;
import java.util.List;
public class KeystoreExportDialog extends Dialog<Keystore> {
public KeystoreExportDialog(Keystore keystore) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(200);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<KeystoreFileExport> exporters = List.of(new Bip129());
Accordion exportAccordion = new Accordion();
for(KeystoreFileExport exporter : exporters) {
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
exportAccordion.getPanes().add(exportPane);
}
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(exportAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(280);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
}
@Subscribe
public void keystoreExported(KeystoreExportEvent event) {
setResult(event.getKeystore());
}
}
@@ -1,20 +1,23 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
public class KeystorePassphraseDialog extends Dialog<String> {
private final CustomPasswordField passphrase;
private final ObjectProperty<byte[]> masterFingerprint = new SimpleObjectProperty<>();
public KeystorePassphraseDialog(Keystore keystore) {
this(null, keystore);
@@ -45,10 +48,38 @@ public class KeystorePassphraseDialog extends Dialog<String> {
content.setPrefHeight(50);
content.getChildren().add(passphrase);
passphrase.textProperty().addListener((observable, oldValue, passphrase) -> {
masterFingerprint.set(getMasterFingerprint(keystore, passphrase));
});
HBox fingerprintBox = new HBox(10);
fingerprintBox.setAlignment(Pos.CENTER_LEFT);
Label fingerprintLabel = new Label("Master fingerprint:");
TextField fingerprintHex = new TextField();
fingerprintHex.setDisable(true);
fingerprintHex.setMaxWidth(80);
fingerprintHex.getStyleClass().addAll("fixed-width");
fingerprintHex.setStyle("-fx-opacity: 0.6");
masterFingerprint.addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
fingerprintHex.setText(Utils.bytesToHex(newValue));
}
});
LifeHashIcon lifeHashIcon = new LifeHashIcon();
lifeHashIcon.dataProperty().bind(masterFingerprint);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("All passphrases create valid wallets." +
"\nThe master fingerprint identifies the keystore and changes as the passphrase changes." +
"\n" + (confirm ? "Take a moment to identify it before proceeding." : "Make sure you recognise it before proceeding."));
fingerprintBox.getChildren().addAll(fingerprintLabel, fingerprintHex, lifeHashIcon, helpLabel);
content.getChildren().add(fingerprintBox);
masterFingerprint.set(getMasterFingerprint(keystore, ""));
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warnGlyph.getStyleClass().add("warn-icon");
warnGlyph.setFontSize(12);
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
Label warnLabel = new Label((confirm ? "Note" : "Check") + " the master fingerprint before proceeding!", warnGlyph);
warnLabel.setGraphicTextGap(5);
content.getChildren().add(warnLabel);
@@ -57,4 +88,14 @@ public class KeystorePassphraseDialog extends Dialog<String> {
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
}
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
try {
Keystore copyKeystore = keystore.copy();
copyKeystore.getSeed().setPassphrase(passphrase);
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
} catch(MnemonicException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,6 +1,10 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.Event;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
@@ -13,9 +17,11 @@ import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
class LabelCell extends TextFieldTreeTableCell<Entry, String> implements ConfirmationsListener {
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
private IntegerProperty confirmationsProperty;
public LabelCell() {
super(new DefaultStringConverter());
getStyleClass().add("label-cell");
@@ -60,6 +66,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
}
super.commitEdit(label);
Platform.runLater(() -> getTreeTableView().requestFocus());
}
@Override
@@ -81,6 +88,21 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
}
}
@Override
public IntegerProperty getConfirmationsProperty() {
if(confirmationsProperty == null) {
confirmationsProperty = new SimpleIntegerProperty();
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
getStyleClass().remove("confirming");
confirmationsProperty.unbind();
}
});
}
return confirmationsProperty;
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
@@ -0,0 +1,70 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.io.ImageUtils;
import com.sparrowwallet.toucan.LifeHash;
import com.sparrowwallet.toucan.LifeHashVersion;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Group;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.util.Arrays;
public class LifeHashIcon extends Group {
private static final Logger log = LoggerFactory.getLogger(LifeHashIcon.class);
private static final int SIZE = 24;
private final ObjectProperty<byte[]> dataProperty = new SimpleObjectProperty<>(null);
public LifeHashIcon() {
super();
dataProperty.addListener((observable, oldValue, data) -> {
if(data == null) {
getChildren().clear();
} else if(oldValue == null || !Arrays.equals(oldValue, data)) {
LifeHash.Image lifeHashImage = LifeHash.makeFromData(data, LifeHashVersion.VERSION2, 1, false);
BufferedImage bufferedImage = LifeHash.getBufferedImage(lifeHashImage);
BufferedImage resizedImage = ImageUtils.resizeToImage(bufferedImage, SIZE, SIZE);
Image image = SwingFXUtils.toFXImage(resizedImage, null);
setImage(image);
}
});
}
private void setImage(Image image) {
getChildren().clear();
Rectangle rectangle = new Rectangle(SIZE, SIZE);
rectangle.setArcWidth(6);
rectangle.setArcHeight(6);
rectangle.setFill(new ImagePattern(image));
rectangle.setStroke(Color.rgb(65, 72, 77));
rectangle.setStrokeWidth(1.0);
getChildren().add(rectangle);
}
public byte[] getData() {
return dataProperty.get();
}
public ObjectProperty<byte[]> dataProperty() {
return dataProperty;
}
public void setData(byte[] data) {
this.dataProperty.set(data);
}
public void setHex(String hex) {
setData(hex == null ? null : Utils.hexToBytes(hex));
}
}
@@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
@@ -269,8 +266,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB && !wallet.getKeystores().get(0).getWalletModel().isCard()) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a connected keystore");
}
}
@@ -295,7 +292,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType() != ScriptType.P2TR && address.getScriptType().isAllowed(PolicyType.SINGLE);
return address.getScriptType() != ScriptType.P2TR && (address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH);
} catch (InvalidAddressException e) {
return false;
}
@@ -319,8 +316,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} else {
signUnencryptedKeystore(signingWallet);
}
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(signingWallet);
} else if(signingWallet.containsSource(KeystoreSource.HW_USB) || wallet.getKeystores().get(0).getWalletModel().isCard()) {
signDeviceKeystore(signingWallet);
}
}
@@ -339,10 +336,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void signUsbKeystore(Wallet usbWallet) {
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
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());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
signature.clear();
@@ -0,0 +1,374 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.PdfUtils;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;
public class MnemonicGridDialog extends Dialog<List<String>> {
private final SpreadsheetView spreadsheetView;
private final int GRID_COLUMN_COUNT = 16;
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
public MnemonicGridDialog() {
DialogPane dialogPane = new MnemonicGridDialogPane();
setDialogPane(dialogPane);
setTitle("Border Wallets Grid");
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. Note the order of selection is important!");
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
dialogPane.setGraphic(new ImageView(image));
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
Grid grid = getGrid(emptyWordGrid);
spreadsheetView = new SpreadsheetView(grid);
spreadsheetView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
try {
Field f = event.getClass().getDeclaredField(Platform.getCurrent() == Platform.OSX ? "metaDown" : "controlDown");
f.setAccessible(true);
f.set(event, true);
} catch(IllegalAccessException | NoSuchFieldException e) {
//ignore
}
});
spreadsheetView.setId("grid");
spreadsheetView.setEditable(false);
spreadsheetView.setFixingColumnsAllowed(false);
spreadsheetView.setFixingRowsAllowed(false);
spreadsheetView.getSelectionModel().getSelectedCells().addListener(new ListChangeListener<>() {
@Override
public void onChanged(Change<? extends TablePosition> c) {
int numWords = c.getList().size();
wordsSelectedProperty.set(numWords == 11 || numWords == 23);
}
});
StackPane stackPane = new StackPane();
stackPane.getChildren().add(spreadsheetView);
dialogPane.setContent(stackPane);
stackPane.widthProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
for(SpreadsheetColumn column : spreadsheetView.getColumns()) {
column.setPrefWidth((newValue.doubleValue() - spreadsheetView.getRowHeaderWidth() - 3) / 17);
}
}
});
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF...", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(loadCsvButtonType);
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP);
dialogPane.getButtonTypes().add(generateButtonType);
final ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear Selection", ButtonBar.ButtonData.OTHER);
dialogPane.getButtonTypes().add(clearButtonType);
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty)));
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? getSelectedWords() : null;
});
dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.moveToActiveWindowScreen(this);
}
private Grid getGrid(String[][] wordGrid) {
int rowCount = wordGrid.length;
int columnCount = wordGrid[0].length;
GridBase grid = new GridBase(rowCount, columnCount);
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
grid.getColumnHeaders().setAll("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P");
for(int i = 0; i < rowCount; i++) {
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
for(int j = 0; j < columnCount; j++) {
list.add(createCell(i, j, wordGrid[i][j]));
}
rows.add(list);
grid.getRowHeaders().add(String.format("%03d", i + 1));
}
grid.setRows(rows);
return grid;
}
private SpreadsheetCell createCell(int row, int column, String word) {
return SpreadsheetCellType.STRING.createCell(row, column, 1, 1, word == null ? "" : word);
}
private List<String> getSelectedWords() {
List<String> abbreviations = spreadsheetView.getSelectionModel().getSelectedCells().stream()
.map(position -> (String)spreadsheetView.getGrid().getRows().get(position.getRow()).get(position.getColumn()).getItem()).collect(Collectors.toList());
List<String> words = new ArrayList<>();
for(String abbreviation : abbreviations) {
for(String word : Bip39MnemonicCode.INSTANCE.getWordList()) {
if((abbreviation.length() == 3 && word.equals(abbreviation)) || (abbreviation.length() >= 4 && word.startsWith(abbreviation))) {
words.add(word);
break;
}
}
}
if(words.size() != abbreviations.size()) {
abbreviations.removeIf(abbr -> words.stream().anyMatch(w -> w.startsWith(abbr)));
throw new IllegalStateException("Could not find words for abbreviations: " + abbreviations);
}
return words;
}
public List<String> shuffle(List<String> mnemonic) {
String mnemonicString = String.join(" ", mnemonic);
List<String> words = new ArrayList<>(Bip39MnemonicCode.INSTANCE.getWordList());
UltraHighEntropyPrng uhePrng = new UltraHighEntropyPrng();
uhePrng.initState();
uhePrng.hashString(mnemonicString);
for(int i = words.size() - 1; i > 0; i--) {
int j = (int)uhePrng.random(i + 1);
String tmp = words.get(i);
words.set(i, words.get(j));
words.set(j, tmp);
}
return words;
}
private String[][] toGrid(List<String> words) {
String[][] grid = new String[words.size()/GRID_COLUMN_COUNT][GRID_COLUMN_COUNT];
int row = 0;
int col = 0;
for(String word : words) {
String abbr = word.length() < 4 ? word : word.substring(0, 4);
grid[row][col] = abbr;
col++;
if(col >= GRID_COLUMN_COUNT) {
col = 0;
row++;
}
}
return grid;
}
private class MnemonicGridDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button loadButton = new Button(buttonType.getText());
loadButton.setGraphicTextGap(5);
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(loadButton, buttonData);
loadButton.setOnAction(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open PDF");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("PDF", "*.pdf")
);
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
if(file != null) {
try(BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
String[][] wordGrid = PdfUtils.getWordGrid(inputStream);
spreadsheetView.setGrid(getGrid(wordGrid));
initializedProperty.set(true);
} catch(Exception e) {
AppServices.showErrorDialog("Cannot load PDF", e.getMessage());
}
}
});
button = loadButton;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP) {
Button generateButton = new Button(buttonType.getText());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(generateButton, buttonData);
generateButton.setOnAction(event -> {
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(12);
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
if(optWords.isPresent()) {
List<String> mnemonicWords = optWords.get();
List<String> shuffledWordList = shuffle(mnemonicWords);
String[][] wordGrid = toGrid(shuffledWordList);
spreadsheetView.setGrid(getGrid(wordGrid));
initializedProperty.set(true);
if(seedEntryDialog.isGenerated()) {
PdfUtils.saveWordGrid(wordGrid, mnemonicWords);
}
}
});
button = generateButton;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OTHER) {
Button clearButton = new Button(buttonType.getText());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(clearButton, buttonData);
clearButton.setOnAction(event -> {
spreadsheetView.getSelectionModel().clearSelection();
});
button = clearButton;
} else {
button = super.createButton(buttonType);
}
return button;
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
public static class UltraHighEntropyPrng {
private final int order;
private double carry;
private int phase;
private final double[] intermediates;
private int i, j, k; // general purpose locals
public UltraHighEntropyPrng() {
order = 48; // set the 'order' number of ENTROPY-holding 32-bit values
carry = 1; // init the 'carry' used by the multiply-with-carry (MWC) algorithm
phase = order; // init the 'phase' (max-1) of the intermediate variable pointer
intermediates = new double[order]; // declare our intermediate variables array
for(i = 0; i < order; i++) {
// Used to simulate javascript's Math.random
Random random = new Random();
intermediates[i] = mash(random.nextInt(Integer.MAX_VALUE)); // fill the array with initial mash hash values
}
}
public double random(int range) {
return Math.floor(range * (rawPrng() + ((long)(rawPrng() * 0x200000L)) * 1.1102230246251565e-16)); // 2^-53
}
private String randomString(int count) {
StringBuilder stringBuilder = new StringBuilder();
for(i = 0; i < count; i++) {
char newChar = (char) (33 + random(94));
stringBuilder.append(newChar);
}
return stringBuilder.toString();
}
private double rawPrng() {
if(++phase >= order) {
phase = 0;
}
double t = 1768863 * intermediates[phase] + carry * 2.3283064365386963e-10; // 2^-32
long temp = (long)t;
return intermediates[phase] = t - (carry = temp);
}
private void hash(String args) {
for(i = 0; i < args.length(); i++) {
for(j = 0; j < order; j++) {
intermediates[j] -= mash(args.charAt(i));
if(intermediates[j] < 0) {
intermediates[j] = intermediates[j] + 1;
}
}
}
}
public void hashString(String input) {
mash(input); // use the string to evolve the 'mash' state
char[] inputAry = input.toCharArray();
for(i = 0; i < inputAry.length; i++) // scan through the characters in our string
{
k = inputAry[i]; // get the character code at the location
for(j = 0; j < order; j++) // "mash" it into the UHEPRNG state
{
intermediates[j] -= mash(k);
if(intermediates[j] < 0) {
intermediates[j] += 1;
}
}
}
}
public void initState() {
mash(null);
for(i = 0; i < order; i++) {
intermediates[i] = mash(' '); // fill the array with initial mash hash values
}
carry = 1; // init our multiply-with-carry carry
phase = order; // init our phase
}
double n = Integer.toUnsignedLong(0xefc8249d);
private double mash(Object data) {
if(data != null) {
String strData = data.toString();
for(int i = 0; i < strData.length(); i++) {
n += strData.charAt(i);
double h = 0.02519603282416938 * n;
n = Integer.toUnsignedLong((int)h);
h -= n;
h *= n;
n = Integer.toUnsignedLong((int)h);
h -= n;
n += h * 0x100000000L; // 2^32
}
return ((long)n) * 2.3283064365386963e-10; // 2^-32
} else {
n = Integer.toUnsignedLong(0xefc8249d);
}
return n;
}
}
}
@@ -1,6 +1,5 @@
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;
@@ -24,7 +23,8 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
showWordList(keystore.getSeed());
}
protected Node getMnemonicWordsEntry(int numWords) {
@Override
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
VBox vBox = new VBox();
vBox.setSpacing(10);
@@ -0,0 +1,95 @@
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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.scene.control.Button;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.List;
public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
private final BooleanProperty validProperty = new SimpleBooleanProperty(false);
private boolean generated;
public MnemonicKeystoreEntryPane(int numWords) {
super(DeterministicSeed.Type.BIP39.getName(), "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
defaultWordSizeProperty.set(numWords);
setDescription("Generate new or enter existing");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords, false, true));
setExpanded(true);
}
@Override
protected List<Node> createRightButtons() {
Button button = new Button("Next");
button.setVisible(false);
return List.of(button);
}
@Override
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
Bip39MnemonicCode.INSTANCE.check(wordEntriesProperty.get());
validChecksum = true;
} catch(MnemonicException e) {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
validProperty.set(validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
}
public void generateNew() {
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstanceStrong();
} catch(NoSuchAlgorithmException e) {
secureRandom = new SecureRandom();
}
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
displayMnemonicCode(deterministicSeed);
generated = true;
}
private void displayMnemonicCode(DeterministicSeed deterministicSeed) {
setDescription("Write down these words");
showHideLink.setVisible(false);
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(deterministicSeed.getMnemonicCode().get(i));
wordEntry.getEditor().setEditable(false);
}
}
public boolean isValid() {
return validProperty.get();
}
public BooleanProperty validProperty() {
return validProperty;
}
public boolean isGenerated() {
return generated;
}
}
@@ -223,7 +223,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private void confirmBackup() {
setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), true, false));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
@@ -12,6 +12,8 @@ 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;
@@ -20,8 +22,8 @@ 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.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@@ -44,7 +46,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected Label invalidLabel;
protected SimpleListProperty<String> wordEntriesProperty;
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
protected IntegerProperty defaultWordSizeProperty;
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
@@ -79,6 +81,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
MenuItem gridItem = new MenuItem("Border Wallets Grid...");
gridItem.setOnAction(event -> {
showGrid();
});
enterMnemonicButton.getItems().add(gridItem);
MenuItem scanItem = new MenuItem("Scan QR...");
scanItem.setOnAction(event -> {
scanQR();
@@ -87,6 +95,42 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
protected void showGrid() {
MnemonicGridDialog mnemonicGridDialog = new MnemonicGridDialog();
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
if(optWords.isPresent()) {
List<String> words = optWords.get();
setContent(getMnemonicWordsEntry(words.size() + 1, true, true));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
if(i < words.size()) {
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();
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
});
service.start();
}
}
}
}
protected void scanQR() {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
@@ -106,7 +150,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size()));
setContent(getMnemonicWordsEntry(words.size(), true, true));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
@@ -119,11 +163,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void enterMnemonic(int numWords) {
setDescription("Generate new or enter existing");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords));
setContent(getMnemonicWordsEntry(numWords, true, true));
setExpanded(true);
}
protected Node getMnemonicWordsEntry(int numWords) {
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
VBox vBox = new VBox();
vBox.setSpacing(10);
@@ -152,10 +196,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
vBox.getChildren().add(wordsPane);
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
if(showPassphrase) {
PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase);
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
}
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 26, 0, 10));
@@ -250,9 +296,14 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
String[] words = clipboard.getString().split("[\\n\\s]");
WordEntry entry = WordEntry.this;
for(String word : words) {
for(int i = 0; i < words.length; i++) {
String word = words[i];
if(entry.nextField != null) {
entry.nextField.requestFocus();
if(i == words.length - 2 && isValid(word)) {
label.requestFocus();
} else {
entry.nextField.requestFocus();
}
}
entry.wordField.setText(word);
@@ -282,7 +333,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
@@ -290,11 +341,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
});
//Show autocomplete for the last word on focus if empty
boolean lastWord = wordNumber == wordEntryList.size() - 1;
if(lastWord) {
wordField.focusedProperty().addListener((observable, oldValue, focused) -> {
if(focused && wordField.getText().isEmpty()) {
autoCompletionBinding.setUserInput("");
}
});
}
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
));
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
@@ -323,13 +383,30 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
private final int wordNumber;
private final ObservableList<String> wordEntryList;
public WordlistSuggestionProvider(List<String> wordList) {
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
this.wordList = wordList;
this.wordNumber = wordNumber;
this.wordEntryList = wordEntryList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
try {
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
if(!request.getUserText().isEmpty()) {
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
}
return possibleLastWords;
} catch(Exception e) {
log.warn("Cannot determine possible last words", e);
}
}
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
@@ -341,26 +418,63 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
return suggestions;
}
private boolean allPreviousWordsValid() {
for(int i = 0; i < wordEntryList.size() - 1; i++) {
if(!WordEntry.isValid(wordEntryList.get(i))) {
return false;
}
}
return true;
}
}
protected class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
private final TextField passphraseField;
public PassphraseEntry() {
public PassphraseEntry(boolean editable) {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label usePassphraseLabel = new Label("Use passphrase?");
usePassphraseLabel.managedProperty().bind(usePassphraseLabel.visibleProperty());
CheckBox usePassphraseCheckbox = new CheckBox(" ");
usePassphraseCheckbox.setDisable(!editable);
usePassphraseCheckbox.managedProperty().bind(usePassphraseCheckbox.visibleProperty());
usePassphraseLabel.visibleProperty().bind(usePassphraseCheckbox.visibleProperty());
usePassphraseCheckbox.setVisible(passphraseProperty.isEmpty().get());
usePassphraseCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
Optional<ButtonType> optType = AppServices.showWarningDialog("Add a passphrase?", "A passphrase is not a password! " +
"Any variation entered in future loads a valid wallet, but with different addresses.\n\n" +
"This feature provides optional added security for advanced users only. Are you sure?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent() && optType.get() == ButtonType.YES) {
usePassphraseCheckbox.setVisible(false);
} else {
usePassphraseCheckbox.setSelected(false);
}
}
});
Label passphraseLabel = new Label("Passphrase:");
passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseLabel.managedProperty().bind(passphraseLabel.visibleProperty());
passphraseField = new TextField();
passphraseField.setPromptText(passphraseProperty.isEmpty().get() ? "Leave blank for none" : "");
passphraseField.setText(passphraseProperty.get());
passphraseField.setDisable(!editable);
passphraseField.managedProperty().bind(passphraseField.visibleProperty());
passphraseLabel.visibleProperty().bind(passphraseField.visibleProperty());
passphraseField.visibleProperty().bind(usePassphraseCheckbox.visibleProperty().not());
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
HelpLabel helpLabel = new HelpLabel();
helpLabel.setPrefHeight(28);
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
helpLabel.setHelpText("Advanced feature: a passphrase provides optional added security, but it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
getChildren().addAll(usePassphraseLabel, usePassphraseCheckbox, passphraseLabel, passphraseField, helpLabel);
}
public TextField getEditor() {
@@ -10,9 +10,11 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -135,7 +137,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
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);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
@@ -183,7 +188,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
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);
Region region = new Region();
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.PayNymImageLoadedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
@@ -121,9 +124,14 @@ public class PayNymAvatar extends StackPane {
Proxy proxy = AppServices.getProxy();
String url = PayNymService.getHostUrl(proxy != null) + "/" + paymentCodeStr + "/avatar";
if(log.isDebugEnabled()) {
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);
paymentCodeCache.put(cacheId, image);
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
return image;
} catch(Exception e) {
log.debug("Error loading PayNym avatar", e);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.google.common.io.Files;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
@@ -14,7 +15,10 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
@@ -42,7 +46,8 @@ import tornadofx.control.Form;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -96,6 +101,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
HBox.setHgrow(key, Priority.ALWAYS);
keyField.getInputs().add(keyBox);
if(CardApi.isReaderAvailable()) {
VBox cardButtonBox = new VBox(5);
Button cardKey = new Button("", getGlyph(FontAwesome5.Glyph.WIFI));
cardKey.setOnAction(event -> unsealPrivateKey());
cardButtonBox.getChildren().add(cardKey);
keyBox.getChildren().add(cardButtonBox);
}
Field keyScriptTypeField = new Field();
keyScriptTypeField.setText("Script Type:");
keyScriptType = new ComboBox<>();
@@ -279,6 +292,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private void unsealPrivateKey() {
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
Optional<DeviceUnsealDialog.DevicePrivateKey> optPrivateKey = deviceUnsealDialog.showAndWait();
if(optPrivateKey.isPresent()) {
DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get();
key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58());
keyScriptType.setValue(devicePrivateKey.scriptType());
}
}
private void createTransaction() {
try {
DumpedPrivateKey privateKey = getPrivateKey();
@@ -286,14 +309,30 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress();
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
Date since = null;
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
WalletBirthDateDialog addressScanDateDialog = new WalletBirthDateDialog(null, true);
Optional<Date> optSince = addressScanDateDialog.showAndWait();
if(optSince.isPresent()) {
since = optSince.get();
}
}
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
});
addressUtxosService.setOnFailed(failedEvent -> {
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
AppServices.showErrorDialog("Error retrieving outputs for address", rootCause.getMessage());
});
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
}
addressUtxosService.start();
} catch(Exception e) {
log.error("Error creating sweep transaction", e);
@@ -321,8 +360,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
return;
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
return;
}
}
Transaction transaction = new Transaction();
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.control;
public enum QRDensity {
NORMAL("Normal", 250),
LOW("Low", 80);
private final String name;
private final int maxFragmentLength;
QRDensity(String name, int maxFragmentLength) {
this.name = name;
this.maxFragmentLength = maxFragmentLength;
}
public String getName() {
return name;
}
public int getMaxFragmentLength() {
return maxFragmentLength;
}
}
@@ -9,6 +9,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
@@ -28,13 +29,13 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Locale;
import java.util.Optional;
@SuppressWarnings("deprecation")
public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 250;
private static final int ANIMATION_PERIOD_MILLIS = 200;
@@ -42,17 +43,20 @@ public class QRDisplayDialog extends Dialog<UR> {
private static final int QR_HEIGHT = 480;
private final UR ur;
private final UREncoder encoder;
private UREncoder encoder;
private final ImageView qrImageView;
private AnimateQRService animateQRService;
private String currentPart;
private boolean addLegacyEncodingOption;
private boolean useLegacyEncoding;
private String[] legacyParts;
private int legacyPartIndex;
private static boolean initialDensityChange;
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
this(UR.fromBytes(type, data), addLegacyEncodingOption);
}
@@ -63,7 +67,8 @@ public class QRDisplayDialog extends Dialog<UR> {
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
this.addLegacyEncodingOption = addLegacyEncodingOption;
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
@@ -88,6 +93,9 @@ public class QRDisplayDialog extends Dialog<UR> {
if(addLegacyEncodingOption) {
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
} else {
final ButtonType densityButtonType = new javafx.scene.control.ButtonType("Change Density", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(densityButtonType);
}
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
@@ -201,6 +209,20 @@ public class QRDisplayDialog extends Dialog<UR> {
}
}
private void changeQRDensity() {
if(animateQRService != null) {
animateQRService.cancel();
}
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
nextPart();
if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
createAnimateQRService();
}
}
private class AnimateQRService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
@@ -220,18 +242,44 @@ public class QRDisplayDialog extends Dialog<UR> {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton legacy = new ToggleButton(buttonType.getText());
legacy.setGraphicTextGap(5);
setLegacyGraphic(legacy, false);
if(addLegacyEncodingOption) {
ToggleButton legacy = new ToggleButton(buttonType.getText());
legacy.setGraphicTextGap(5);
setLegacyGraphic(legacy, false);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(legacy, buttonData);
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseLegacyEncoding(newValue);
setLegacyGraphic(legacy, newValue);
});
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(legacy, buttonData);
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseLegacyEncoding(newValue);
setLegacyGraphic(legacy, newValue);
});
return legacy;
return legacy;
} else {
Button density = new Button(buttonType.getText());
density.setPrefWidth(160);
density.setGraphicTextGap(5);
updateDensityButton(density);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(density, buttonData);
density.setOnAction(event -> {
if(!initialDensityChange && !encoder.isSinglePart()) {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
initialDensityChange = true;
} else {
return;
}
}
Config.get().setQrDensity(Config.get().getQrDensity() == QRDensity.NORMAL ? QRDensity.LOW : QRDensity.NORMAL);
updateDensityButton(density);
changeQRDensity();
});
return density;
}
}
return super.createButton(buttonType);
@@ -245,10 +293,19 @@ public class QRDisplayDialog extends Dialog<UR> {
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
private void updateDensityButton(Button density) {
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Decrease Density" : "Increase Density");
if(Config.get().getQrDensity() == QRDensity.NORMAL) {
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_PLUS));
} else {
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_MINUS));
}
}
}
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
@@ -53,6 +53,7 @@ import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@SuppressWarnings("deprecation")
@@ -259,7 +260,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
DeterministicSeed seed;
try {
extendedKey = ExtendedKey.fromDescriptor(qrtext);
result = new Result(extendedKey);
result = new Result(extendedKey, null);
return;
} catch(Exception e) {
//Ignore, not a valid xpub
@@ -408,7 +409,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
return new Result(extendedKey);
return new Result(extendedKey, cryptoHDKey.getName());
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
@@ -542,7 +543,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
if(cryptoKeypath != null) {
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
if(cryptoKeypath.getComponents().stream().anyMatch(PathComponent::isWildcard)) {
throw new IllegalArgumentException("Wildcard derivation paths are not supported");
}
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
}
return null;
@@ -656,6 +662,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public final PSBT psbt;
public final BitcoinURI uri;
public final ExtendedKey extendedKey;
public final String extendedKeyName;
public final OutputDescriptor outputDescriptor;
public final List<Wallet> wallets;
public final DeterministicSeed seed;
@@ -667,6 +674,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -679,6 +687,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = psbt;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -691,6 +700,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = uri;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -703,6 +713,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -710,11 +721,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.exception = null;
}
public Result(ExtendedKey extendedKey) {
public Result(ExtendedKey extendedKey, String name) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = extendedKey;
this.extendedKeyName = name;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -727,6 +739,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = outputDescriptor;
this.wallets = null;
this.seed = null;
@@ -739,6 +752,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = wallets;
this.seed = null;
@@ -751,6 +765,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = seed;
@@ -763,6 +778,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -775,6 +791,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.extendedKeyName = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
@@ -58,6 +58,8 @@ public class ScriptArea extends CodeArea {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isString()) {
append(chunk.toString(), "script-other");
} else if(chunk.isScript()) {
Script nestedScript = chunk.getScript();
if (nestedScript.equals(redeemScript)) {
@@ -74,7 +74,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
results = new CoinTreeTable();
results.setShowRoot(false);
results.setPrefWidth(showWallet ? 950 : 850);
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
results.setUnitFormat(walletForms.iterator().next().getWallet());
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
results.setPlaceholder(new Label("No results"));
@@ -0,0 +1,91 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.List;
public class SeedEntryDialog extends Dialog<List<String>> {
private final MnemonicKeystoreEntryPane keystorePane;
public SeedEntryDialog(int numWords) {
final DialogPane dialogPane = new MnemonicGridDialogPane();
setDialogPane(dialogPane);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
int lines = numWords / 3;
int height = lines * 40;
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.getStyleClass().add("edge-to-edge");
scrollPane.setPrefHeight(104 + height);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
Accordion keystoreAccordion = new Accordion();
scrollPane.setContent(keystoreAccordion);
keystorePane = new MnemonicKeystoreEntryPane(numWords);
keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane);
stackPane.getChildren().addAll(anchorPane);
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
okButton.disableProperty().bind(keystorePane.validProperty().not());
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate New", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(generateButtonType);
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? keystorePane.wordEntriesProperty.get() : null;
});
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(180 + height);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
}
private class MnemonicGridDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button generateButton = new Button(buttonType.getText());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(generateButton, buttonData);
generateButton.setOnAction(event -> {
keystorePane.generateNew();
});
button = generateButton;
} else {
button = super.createButton(buttonType);
}
return button;
}
}
public boolean isGenerated() {
return keystorePane.isGenerated();
}
}
@@ -11,10 +11,13 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Optional;
public class TextAreaDialog extends Dialog<String> {
@@ -22,6 +25,8 @@ public class TextAreaDialog extends Dialog<String> {
private final TextArea textArea;
private final String defaultValue;
private final String fileName;
private final byte[] fileBytes;
public TextAreaDialog() {
this("");
@@ -31,7 +36,11 @@ public class TextAreaDialog extends Dialog<String> {
this(defaultValue, true);
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
public TextAreaDialog(String defaultValue, boolean editable) {
this(defaultValue, editable, null, null);
}
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable, @NamedArg("fileName") String fileName, @NamedArg("fileBytes") byte[] fileBytes) {
final DialogPane dialogPane = new TextAreaDialogPane();
setDialogPane(dialogPane);
@@ -48,6 +57,8 @@ public class TextAreaDialog extends Dialog<String> {
HBox.setHgrow(this.textArea, Priority.ALWAYS);
this.defaultValue = defaultValue;
this.fileName = fileName;
this.fileBytes = fileBytes;
dialogPane.setContent(hbox);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
@@ -60,6 +71,9 @@ public class TextAreaDialog extends Dialog<String> {
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(scanButtonType);
} else if(fileBytes != null) {
final ButtonType saveButtonType = new javafx.scene.control.ButtonType("Save File...", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(saveButtonType);
}
Platform.runLater(textArea::requestFocus);
@@ -86,7 +100,7 @@ public class TextAreaDialog extends Dialog<String> {
@Override
protected Node createButton(ButtonType buttonType) {
Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT && buttonType.getText().equals("Scan QR")) {
Button scanButton = new Button(buttonType.getText());
scanButton.setGraphicTextGap(5);
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
@@ -118,6 +132,38 @@ public class TextAreaDialog extends Dialog<String> {
});
button = scanButton;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT && buttonType.getText().equals("Save File...")) {
Button saveButton = new Button(buttonType.getText());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(saveButton, buttonData);
saveButton.setOnAction(event -> {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save File");
if(fileName != null) {
fileChooser.setInitialFileName(fileName);
}
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try {
try(OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(fileBytes);
outputStream.flush();
}
} catch(IOException e) {
log.error("Error saving file", e);
AppServices.showErrorDialog("Error saving file", "Cannot write to " + file.getAbsolutePath());
}
}
});
button = saveButton;
} else {
button = super.createButton(buttonType);
}
@@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TextfieldDialog extends Dialog<String> {
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
private final TextField textField;
private final String defaultValue;
public TextfieldDialog() {
this("");
}
public TextfieldDialog(String defaultValue) {
this(defaultValue, true);
}
public TextfieldDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
final DialogPane dialogPane = getDialogPane();
setDialogPane(dialogPane);
Image image = new Image("/image/sparrow-small.png");
dialogPane.setGraphic(new ImageView(image));
HBox hbox = new HBox();
this.textField = new TextField(defaultValue);
this.textField.setMaxWidth(Double.MAX_VALUE);
this.textField.setEditable(editable);
hbox.getChildren().add(textField);
HBox.setHgrow(this.textField, Priority.ALWAYS);
this.defaultValue = defaultValue;
dialogPane.setContent(hbox);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().add(ButtonType.OK);
if(editable) {
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
}
Platform.runLater(textField::requestFocus);
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? textField.getText() : null;
});
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(230);
AppServices.moveToActiveWindowScreen(this);
}
public final TextField getEditor() {
return textField;
}
public final String getDefaultValue() {
return defaultValue;
}
}
@@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.geometry.Insets;
@@ -106,13 +108,17 @@ public class TitledDescriptionPane extends TitledPane {
protected void setDescription(String text) {
descriptionLabel.getStyleClass().remove("description-error");
descriptionLabel.getStyleClass().add("description-label");
if(!descriptionLabel.getStyleClass().contains("description-label")) {
descriptionLabel.getStyleClass().add("description-label");
}
descriptionLabel.setText(text);
}
protected void setError(String title, String detail) {
descriptionLabel.getStyleClass().remove("description-label");
descriptionLabel.getStyleClass().add("description-error");
if(!descriptionLabel.getStyleClass().contains("description-error")) {
descriptionLabel.getStyleClass().add("description-error");
}
descriptionLabel.setText(title);
if(detail != null && !detail.isEmpty()) {
setContent(getContentBox(detail));
@@ -159,4 +165,17 @@ public class TitledDescriptionPane extends TitledPane {
}
});
}
protected static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
if(wallet == null || requiredDerivation == null) {
return 0;
}
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
if(account < 0) {
account = 0;
}
return account;
}
}
@@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
@@ -33,15 +34,15 @@ public class TorStatusLabel extends Label {
}
public void update() {
boolean proxyInUse = AppServices.isUsingProxy();
boolean internal = AppServices.isTorRunning();
if(!proxyInUse || internal) {
if(!Config.get().isUseProxy()) {
torConnectionTest.cancel();
if(internal) {
if(AppServices.isTorRunning()) {
setTooltip(new Tooltip("Internal Tor proxy enabled"));
}
} else if(!torConnectionTest.isRunning()) {
if(torConnectionTest.getState() == Worker.State.CANCELLED || torConnectionTest.getState() == Worker.State.FAILED) {
torConnectionTest.reset();
}
torConnectionTest.setPeriod(Duration.seconds(20.0));
torConnectionTest.setBackoffStrategy(null);
torConnectionTest.setOnSucceeded(workerStateEvent -> {
@@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.Theme;
@@ -21,6 +21,7 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -28,15 +29,17 @@ import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
@@ -45,11 +48,14 @@ import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import javax.imageio.ImageIO;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.control.CoinLabel.BTC_FORMAT;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
@@ -69,13 +75,14 @@ public class TransactionDiagram extends GridPane {
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
private boolean expanded;
private TransactionDiagram expandedDiagram;
private ContextMenu contextMenu;
private final EventHandler<MouseEvent> expandedDiagramHandler = new EventHandler<>() {
@Override
public void handle(MouseEvent event) {
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
Stage stage = new Stage(StageStyle.UNDECORATED);
stage.setTitle(walletTx.getPayments().iterator().next().getLabel());
stage.setTitle(getDiagramTitle());
stage.initOwner(TransactionDiagram.this.getScene().getWindow());
stage.initModality(Modality.WINDOW_MODAL);
stage.setResizable(false);
@@ -98,7 +105,8 @@ public class TransactionDiagram extends GridPane {
expandedDiagram = new TransactionDiagram();
expandedDiagram.setId("transactionDiagram");
expandedDiagram.setExpanded(true);
updateExpandedDiagram();
expandedDiagram.setFinal(isFinal());
updateDerivedDiagram(expandedDiagram);
HBox buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT);
@@ -125,6 +133,13 @@ public class TransactionDiagram extends GridPane {
}
};
private final EventHandler<ContextMenuEvent> contextMenuHandler = new EventHandler<>() {
@Override
public void handle(ContextMenuEvent event) {
contextMenu.show(getChildren().iterator().next(), event.getScreenX(), event.getScreenY());
}
};
public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight());
@@ -136,7 +151,7 @@ public class TransactionDiagram extends GridPane {
update();
setOnMouseClicked(expandedDiagramHandler);
if(expandedDiagram != null) {
updateExpandedDiagram();
updateDerivedDiagram(expandedDiagram);
}
}
}
@@ -165,20 +180,22 @@ public class TransactionDiagram extends GridPane {
getChildren().clear();
}
private void updateExpandedDiagram() {
expandedDiagram.setFinal(isFinal());
expandedDiagram.setOptimizationStrategy(getOptimizationStrategy());
expandedDiagram.walletTx = walletTx;
private void updateDerivedDiagram(TransactionDiagram diagram) {
diagram.setOptimizationStrategy(getOptimizationStrategy());
diagram.walletTx = walletTx;
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = expandedDiagram.getDisplayedUtxoSets();
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
expandedDiagram.setMinHeight(diagramHeight);
expandedDiagram.setMaxHeight(diagramHeight);
expandedDiagram.update();
if(diagram.isExpanded()) {
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = diagram.getDisplayedUtxoSets();
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
diagram.setMinHeight(diagramHeight);
diagram.setMaxHeight(diagramHeight);
}
if(expandedDiagram.getScene() != null && expandedDiagram.getScene().getWindow() instanceof Stage stage) {
diagram.update();
if(diagram.getScene() != null && diagram.getScene().getWindow() instanceof Stage stage) {
stage.sizeToScene();
}
}
@@ -208,6 +225,17 @@ public class TransactionDiagram extends GridPane {
getChildren().clear();
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
if(contextMenu == null) {
contextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem("Save as Image...");
menuItem.setOnAction(event -> {
contextMenu.hide();
saveAsImage();
});
contextMenu.getItems().add(menuItem);
setOnContextMenuRequested(contextMenuHandler);
}
}
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
@@ -434,7 +462,7 @@ public class TransactionDiagram extends GridPane {
StringJoiner joiner = new StringJoiner("\n");
joiner.add("Spending " + getSatsValue(inputValue) + " sats from" + (isExpanded() ? ":" : " (click to expand):"));
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(additionalInput.getHashAsString() + ":" + additionalInput.getIndex());
joiner.add(getInputDescription(additionalInput));
}
tooltip.setText(joiner.toString());
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
@@ -797,12 +825,51 @@ public class TransactionDiagram extends GridPane {
tooltip.setShowDuration(Duration.INDEFINITE);
tooltip.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip);
txPane.getChildren().add(txLabel);
txPane.getChildren().add(createSpacer());
return txPane;
}
private void saveAsImage() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save Image");
fileChooser.setInitialFileName(getDiagramTitle() + ".png");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
TransactionDiagram transactionDiagram = new TransactionDiagram();
transactionDiagram.setId("transactionDiagram");
transactionDiagram.setFinal(true);
transactionDiagram.setExpanded(isExpanded());
transactionDiagram.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
transactionDiagram.setStyle("-fx-text-background-color: " + (Config.get().getTheme() == Theme.DARK ? "#ffffff" : "#000000"));
updateDerivedDiagram(transactionDiagram);
Scene scene = new Scene(transactionDiagram);
scene.setFill(Color.TRANSPARENT);
scene.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
scene.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
scene.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
Image image = scene.snapshot(null);
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null);
try {
ImageIO.write(bufferedImage, "png", file);
} catch(IOException e) {
AppServices.showErrorDialog("Error saving image", e.getMessage());
}
}
}
private String getDiagramTitle() {
if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) {
return walletTx.getPayments().get(0).getLabel();
} else {
return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]";
}
}
public double getDiagramHeight() {
if(isExpanded()) {
return getMaxHeight();
@@ -1244,6 +1311,14 @@ public class TransactionDiagram extends GridPane {
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyAddress);
MenuItem showAddress = new MenuItem("Show Address as QR...");
showAddress.setOnAction(event -> {
hide();
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(address.toString());
qrDisplayDialog.showAndWait();
});
getItems().add(showAddress);
}
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
@@ -1257,7 +1332,8 @@ public class TransactionDiagram extends GridPane {
copyBtcValue.setOnAction(event -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(BTC_FORMAT.format((double)value / Transaction.SATOSHIS_PER_BITCOIN));
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
content.putString(format.formatBtcValue(value));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
@@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
@@ -8,12 +7,10 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.List;
public class TransactionsTreeTable extends CoinTreeTable {
public void initialize(WalletTransactionsEntry rootEntry) {
getStyleClass().add("transactions-treetable");
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
updateAll(rootEntry);
setShowRoot(false);
@@ -58,7 +55,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
}
public void updateAll(WalletTransactionsEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
@@ -63,6 +63,8 @@ public class TrayManager {
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
miStage.addActionListener(e -> Platform.runLater(() -> {
stage.show();
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
EventQueue.invokeLater(() -> {
popupMenu.remove(miStage);
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
@@ -36,8 +37,7 @@ public class UtxosChart extends BarChart<String, Number> {
getData().add(utxoSeries);
update(walletUtxosEntry);
BitcoinUnit unit = Config.get().getBitcoinUnit();
setBitcoinUnit(walletUtxosEntry.getWallet(), unit);
setUnitFormat(walletUtxosEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
}
public void update(WalletUtxosEntry walletUtxosEntry) {
@@ -117,12 +117,16 @@ public class UtxosChart extends BarChart<String, Number> {
this.selectedEntries = entries;
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
if(format == null) {
format = UnitFormat.DOT;
}
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
}
}
@@ -1,20 +1,17 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator;
import java.util.List;
public class UtxosTreeTable extends CoinTreeTable {
public void initialize(WalletUtxosEntry rootEntry) {
getStyleClass().add("utxos-treetable");
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
updateAll(rootEntry);
setShowRoot(false);
@@ -93,7 +90,7 @@ public class UtxosTreeTable extends CoinTreeTable {
}
public void updateAll(WalletUtxosEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
setUnitFormat(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
@@ -21,12 +21,12 @@ import java.util.Date;
public class WalletBirthDateDialog extends Dialog<Date> {
private final DatePicker birthDatePicker;
public WalletBirthDateDialog(Date birthDate) {
public WalletBirthDateDialog(Date birthDate, boolean singleAddress) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
@@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
));
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
final ButtonType okButtonType = new javafx.scene.control.ButtonType(singleAddress ? "Scan Address" : "Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
@@ -42,17 +42,19 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
Accordion exportAccordion = new Accordion();
for (WalletExport exporter : exporters) {
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
exportAccordion.getPanes().add(exportPane);
for(WalletExport exporter : exporters) {
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
exportAccordion.getPanes().add(exportPane);
}
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
@@ -0,0 +1,215 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ImageUtils;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;
import org.controlsfx.glyphfont.Glyph;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
public class WalletIcon extends StackPane {
public static final String PROTOCOL = "walleticon";
private static final String QUERY = "icon";
public static final int WIDTH = 15;
public static final int HEIGHT = 15;
public static final int SAVE_WIDTH = WIDTH * 2;
public static final int SAVE_HEIGHT = HEIGHT * 2;
private final Storage storage;
private final ObjectProperty<Wallet> walletProperty = new SimpleObjectProperty<>();
private final Keystore keystore;
private final Glyph fallbackIcon;
public WalletIcon(Storage storage, Wallet wallet) {
this(storage, wallet, null, null);
}
public WalletIcon(Storage storage, Wallet wallet, Keystore keystore, Glyph fallbackIcon) {
super();
this.storage = storage;
this.keystore = keystore;
this.fallbackIcon = fallbackIcon;
setPrefSize(WIDTH, HEIGHT);
walletProperty.addListener((observable, oldValue, newValue) -> {
refresh();
});
walletProperty.set(wallet);
}
public void refresh() {
Wallet wallet = getWallet();
getChildren().clear();
if(wallet.getWalletConfig() != null && wallet.getWalletConfig().getIconData() != null) {
String walletId = storage.getWalletId(wallet);
if(AppServices.get().getWallet(walletId) != null) {
addWalletIcon(walletId);
} else {
Platform.runLater(() -> addWalletIcon(walletId));
}
} else if(this.keystore != null || wallet.getKeystores().size() == 1) {
Keystore keystore = this.keystore == null ? wallet.getKeystores().get(0) : this.keystore;
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getSource() == KeystoreSource.HW_AIRGAPPED) {
WalletModel walletModel = keystore.getWalletModel();
SVGImage svgImage;
if(Config.get().getTheme() == Theme.DARK) {
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg");
} else {
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
}
if(svgImage != null) {
getChildren().add(svgImage);
return;
}
Image image = null;
if(Config.get().getTheme() == Theme.DARK) {
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
}
if(image == null) {
image = loadImage("image/" + walletModel.getType() + "-icon.png");
}
if(image == null) {
image = loadImage("image/" + walletModel.getType() + ".png");
}
if(image != null && !image.isError()) {
ImageView imageView = new ImageView(image);
getChildren().add(imageView);
}
}
}
if(getChildren().isEmpty()) {
Glyph glyph = fallbackIcon;
if(glyph == null) {
glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
}
glyph.setFontSize(10.0);
getChildren().add(glyph);
}
}
private SVGImage loadSVGImage(String imageName) {
try {
URL url = AppServices.class.getResource(imageName);
if(url != null) {
return SVGLoader.load(url);
}
} catch(Exception e) {
//ignore
}
return null;
}
private Image loadImage(String imageName) {
try {
return new Image(imageName, 15, 15, true, true);
} catch(Exception e) {
//ignore
}
return null;
}
private void addWalletIcon(String walletId) {
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
getChildren().clear();
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
circle.setFill(new ImagePattern(image));
getChildren().add(circle);
}
public boolean addFailure() {
if(getChildren().stream().noneMatch(node -> node.getStyleClass().contains("failure"))) {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.setFontSize(10);
failGlyph.getStyleClass().add("failure");
getChildren().add(failGlyph);
StackPane.setAlignment(failGlyph, Pos.TOP_RIGHT);
failGlyph.setTranslateX(5);
failGlyph.setTranslateY(-4);
return true;
}
return false;
}
public void removeFailure() {
getChildren().removeIf(node -> node.getStyleClass().contains("failure"));
}
public Wallet getWallet() {
return walletProperty.get();
}
public ObjectProperty<Wallet> walletProperty() {
return walletProperty;
}
public void setWallet(Wallet wallet) {
this.walletProperty.set(wallet);
}
public static class WalletIconStreamHandler extends URLStreamHandler {
@Override
protected URLConnection openConnection(URL url) throws IOException {
return new URLConnection(url) {
@Override
public void connect() throws IOException {
//Nothing required
}
@Override
public InputStream getInputStream() throws IOException {
String walletId = url.getPath();
String query = url.getQuery();
Wallet wallet = AppServices.get().getWallet(walletId);
if(wallet == null) {
throw new IOException("Cannot find wallet for wallet id " + walletId);
}
if(query.startsWith(QUERY)) {
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
throw new IOException("No icon data for " + walletId);
}
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
if(query.endsWith("@2x")) {
return bais;
} else {
return ImageUtils.resize(bais, WalletIcon.WIDTH, WalletIcon.HEIGHT);
}
}
throw new MalformedURLException("Cannot load url " + url);
}
};
}
}
}
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
@@ -16,6 +17,7 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -24,7 +26,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
private final Accordion importAccordion;
private final Button scanButton;
public WalletImportDialog() {
public WalletImportDialog(List<WalletForm> selectedWalletForms) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
@@ -49,16 +51,23 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
}
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow()));
if(!selectedWalletForms.isEmpty()) {
walletImporters.add(new WalletLabels(selectedWalletForms));
}
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
@@ -34,6 +34,14 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
}
public WalletNameDialog(String initialName) {
this(initialName, false);
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions) {
this(initialName, hasExistingTransactions, null);
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
@@ -91,6 +99,12 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
if(requestBirthDate) {
content.getChildren().add(existingBox);
if(hasExistingTransactions) {
existingCheck.setSelected(true);
}
if(startDate != null) {
existingPicker.setValue(startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
}
dialogPane.setContent(content);
@@ -1,11 +1,19 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.io.Config;
public class BitcoinUnitChangedEvent {
public class BitcoinUnitChangedEvent extends UnitFormatChangedEvent {
private final BitcoinUnit bitcoinUnit;
public BitcoinUnitChangedEvent(BitcoinUnit bitcoinUnit) {
super(Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat());
this.bitcoinUnit = bitcoinUnit;
}
public BitcoinUnitChangedEvent(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
super(unitFormat);
this.bitcoinUnit = bitcoinUnit;
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.sparrow.net.TlsServerException;
public class ConnectionFailedEvent {
@@ -20,6 +21,12 @@ public class ConnectionFailedEvent {
Throwable cause = (exception.getCause() != null ? exception.getCause() : exception);
cause = (cause.getCause() != null ? cause.getCause() : cause);
if(cause instanceof JsonRpcException jsonRpcException && jsonRpcException.getErrorMessage() != null) {
return jsonRpcException.getErrorMessage().getMessage() +
(jsonRpcException.getErrorMessage().getData() != null ? " (" + jsonRpcException.getErrorMessage().getData().asText() + ")" : "");
}
String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error"));
return message + " (" + cause.getMessage() + ")";
}
@@ -0,0 +1,53 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CormorantPruneStatusEvent extends CormorantStatusEvent {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");
private final Wallet wallet;
private final Date scanDate;
private final Date pruneDate;
private final boolean legacyWalletExists;
public CormorantPruneStatusEvent(String status, Wallet wallet, Date scanDate, Date pruneDate, boolean legacyWalletExists) {
super(status);
this.wallet = wallet;
this.scanDate = scanDate;
this.pruneDate = pruneDate;
this.legacyWalletExists = legacyWalletExists;
}
@Override
public boolean isFor(Wallet wallet) {
return this.wallet == wallet;
}
public Wallet getWallet() {
return wallet;
}
public Date getScanDate() {
return scanDate;
}
public String getScanDateAsString() {
return DATE_FORMAT.format(scanDate);
}
public Date getPruneDate() {
return pruneDate;
}
public String getPruneDateAsString() {
return DATE_FORMAT.format(pruneDate);
}
public boolean legacyWalletExists() {
return legacyWalletExists;
}
}
@@ -0,0 +1,48 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.time.Duration;
import java.util.Set;
public class CormorantScanStatusEvent extends CormorantStatusEvent {
private final Set<Wallet> scanningWallets;
private final int progress;
private final Duration remainingDuration;
public CormorantScanStatusEvent(String status, Set<Wallet> scanningWallets, int progress, Duration remainingDuration) {
super(status);
this.scanningWallets = scanningWallets;
this.progress = progress;
this.remainingDuration = remainingDuration;
}
@Override
public boolean isFor(Wallet wallet) {
return scanningWallets.contains(wallet);
}
public int getProgress() {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public Duration getRemaining() {
return remainingDuration;
}
public String getRemainingAsString() {
if(remainingDuration != null) {
if(progress < 30) {
return Math.round((double)remainingDuration.toSeconds() / 60) + "m";
} else {
return remainingDuration.toMinutesPart() + "m " + remainingDuration.toSecondsPart() + "s";
}
}
return "";
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public abstract class CormorantStatusEvent {
private final String status;
public CormorantStatusEvent(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public abstract boolean isFor(Wallet wallet);
}
@@ -0,0 +1,41 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CormorantSyncStatusEvent extends CormorantStatusEvent {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm");
private final int progress;
private final Date tip;
public CormorantSyncStatusEvent(String status, int progress, Date tip) {
super(status);
this.progress = progress;
this.tip = tip;
}
@Override
public boolean isFor(Wallet wallet) {
return true;
}
public int getProgress() {
return progress;
}
public boolean isCompleted() {
return progress == 100;
}
public Date getTip() {
return tip;
}
public String getTipAsString() {
return tip == null ? "" : DATE_FORMAT.format(tip);
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.address.Address;
public class DeviceAddressEvent {
private final Address address;
public DeviceAddressEvent(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
public class DeviceGetPrivateKeyEvent {
private final ECKey privateKey;
private final ScriptType scriptType;
public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) {
this.privateKey = privateKey;
this.scriptType = scriptType;
}
public ECKey getPrivateKey() {
return privateKey;
}
public ScriptType getScriptType() {
return scriptType;
}
}
@@ -0,0 +1,21 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class ExistingWalletImportedEvent {
private final String existingWalletId;
private final Wallet importedWallet;
public ExistingWalletImportedEvent(String existingWalletId, Wallet importedWallet) {
this.existingWalletId = existingWalletId;
this.importedWallet = importedWallet;
}
public String getExistingWalletId() {
return existingWalletId;
}
public Wallet getImportedWallet() {
return importedWallet;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
public class KeystoreExportEvent {
private final Keystore keystore;
public KeystoreExportEvent(Keystore keystore) {
this.keystore = keystore;
}
public Keystore getKeystore() {
return keystore;
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class NewChildWalletSavedEvent {
private final Storage storage;
private final Wallet masterWallet;
private final Wallet childWallet;
public NewChildWalletSavedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
this.storage = storage;
this.masterWallet = masterWallet;
this.childWallet = childWallet;
}
public String getMasterWalletId() {
return storage.getWalletId(masterWallet);
}
public Wallet getChildWallet() {
return childWallet;
}
}

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