Compare commits

...
208 Commits
Author SHA1 Message Date
Craig Raw fa082e892f about dialog, older mac hwi build 2020-08-21 20:58:11 +02:00
Craig Raw c814b8e350 various fixes, better non-electrumx support 2020-08-21 17:16:14 +02:00
Craig Raw 2b48b4cd6e better handling of server connection editing 2020-08-21 10:48:41 +02:00
Craig Raw d49f85fe5b more import fixes 2020-08-21 10:08:43 +02:00
Craig Raw af525797ff improvements to importing and exporting 2020-08-21 09:59:11 +02:00
Craig Raw e9e10de266 export file naming with correct extensions 2020-08-20 18:20:42 +02:00
Craig Raw ab5fdef919 display full derivation path where possible 2020-08-20 17:59:58 +02:00
Craig Raw f723d20c9a disable absolute locktime if sequence disabled 2020-08-20 17:40:56 +02:00
Craig Raw 03c488cf4d add warnings to block time, set current height, close once 2020-08-20 17:30:39 +02:00
Craig Raw ca64a1d307 scan qr to bitcoin uri 2020-08-20 15:26:59 +02:00
Craig Raw 4c8f7bb711 clear fiat amounts 2020-08-20 12:30:21 +02:00
Craig Raw 77dd8ca5d7 unregister closed tab and dialog subscribers, fix connection error reset behaviour 2020-08-20 11:25:34 +02:00
Craig Raw d5aba35184 make controller and tab data share data sources 2020-08-19 15:44:40 +02:00
Craig Raw 30e2ab5e2a fill tx labels on send 2020-08-19 08:40:06 +02:00
Craig Raw e93fcf7fbd show fiat fee amount 2020-08-18 12:54:43 +02:00
Craig Raw 198672a06e further keystore improvements 2020-08-18 11:50:27 +02:00
Craig Raw 133d771c09 keystore buttons label tooltips 2020-08-18 11:02:45 +02:00
Craig Raw 9a80065605 connection toggle tooltip 2020-08-18 10:55:07 +02:00
Craig Raw ccf4e98968 ellipsis and incomplete wallet background save fixes 2020-08-18 10:47:32 +02:00
Craig Raw df15129f64 bip39 word list improvements #2 2020-08-18 10:20:56 +02:00
Craig Raw e878648587 bip39 word list usability improvements 2020-08-17 19:42:30 +02:00
Craig Raw 75bf63d936 fix wallet settings update bugs 2020-08-17 17:25:33 +02:00
Craig Raw 304bdd5c48 remove sys err exception logging 2020-08-17 17:00:48 +02:00
Craig Raw e1934c99bc upgrade controlsfx, fix notifications bug 2020-08-17 16:48:31 +02:00
Craig Raw fa7d09c5a6 add non batched electrum server rpc 2020-08-10 18:58:47 +02:00
Craig Raw 79c2e29e34 refactor rpc elements out 2020-08-10 16:56:34 +02:00
Craig Raw bb635a6c68 refactor electrumserver into net package 2020-08-10 09:38:15 +02:00
Craig Raw 7d8774c94c better handling of showing blocktransaction details 2020-08-10 08:34:47 +02:00
Craig Raw f35b3d46a1 show change output when signing 2020-08-09 17:03:38 +02:00
Craig Raw f3a7d583a5 initial setup and drag file improvements 2020-08-09 15:26:38 +02:00
Craig Raw 37c3d573e0 wallet load and dialog field improvements 2020-08-09 14:46:52 +02:00
Craig Raw b2f48a1b05 improve new tx notifications 2020-08-09 14:13:10 +02:00
Craig Raw f0b7409c4a make usb sign dialog non modal 2020-08-09 12:45:09 +02:00
Craig Raw a8612da463 various fixes to signing 2020-08-09 12:23:59 +02:00
Craig Raw f2afd7cb69 faster test with --version 2020-08-07 18:40:58 +02:00
Craig Raw 42d40b8171 upgrade to hwi 1.1.2 2020-08-07 18:07:39 +02:00
Craig Raw cda4e30e3f auto open recent wallets 2020-08-07 17:31:40 +02:00
Craig Raw b43b25769e fix receive pane layout 2020-08-07 17:10:32 +02:00
Craig Raw 84d08fea18 display address on device, device support improvements 2020-08-07 14:56:01 +02:00
Craig Raw 8aed206a34 notifications preference 2020-08-07 10:26:26 +02:00
Craig Raw 08498b1b71 hi res images 2020-08-07 10:02:57 +02:00
Craig Raw 10a684cf41 wallet update notifications 2020-08-06 17:52:18 +02:00
Craig Raw 32d25286a6 store passphrase in memory as securestring 2020-08-06 10:27:03 +02:00
Craig Raw 2a6a296cf3 various bugs fixed 2020-08-06 09:23:45 +02:00
Craig Raw 53478d9b22 show seed words dialog, fix electrum import 2020-08-05 16:55:18 +02:00
Craig Raw ce60fe3dc6 extra logging, backup existing wallet first functionality 2020-08-05 13:58:54 +02:00
Craig Raw 03abc59ed3 fix ssl handshake issue 2020-08-05 12:36:50 +02:00
Craig Raw e7c83e1d3f fix encrypted storage test 2020-08-05 10:40:33 +02:00
Craig Raw 827d35cba3 add file logging 2020-08-05 10:25:38 +02:00
Craig Raw 3556e4abc9 configure file logging 2020-08-05 09:12:06 +02:00
Craig Raw 73a1ab13a1 no ds store 2020-08-04 12:50:07 +02:00
Craig Raw c9c00cc74e add qr scanning support 2020-08-04 12:47:29 +02:00
Craig Raw 709c65ec20 qr code display and random sampler port 2020-08-03 10:30:30 +02:00
Craig Raw 4068a6c541 add licensing for urkit 2020-08-02 18:03:24 +02:00
Craig Raw 2f8d40198e port of URKit 2020-08-02 17:54:51 +02:00
Craig Raw d50ed46caf save tx and minor fixes 2020-07-31 11:48:44 +02:00
Craig Raw e5327b3d3d broadcast fixes 2020-07-31 10:36:59 +02:00
Craig Raw 39e5031a78 remove ds store 2020-07-30 15:58:47 +02:00
Craig Raw 928731b383 signing multisig fixes 2020-07-30 15:58:19 +02:00
Craig Raw c9d7b03afc broadcast tx 2020-07-30 12:18:45 +02:00
Craig Raw 75f5fd2e12 combining and finalising psbt fixes 2020-07-30 10:00:09 +02:00
Craig Raw 29cfda7908 view extracted transaction from psbt 2020-07-28 13:14:11 +02:00
Craig Raw 658ab2f81c finalize psbt once signing animation is complete 2020-07-28 08:31:11 +02:00
Craig Raw 62618e5da5 finalize form ui change 2020-07-27 14:10:22 +02:00
Craig Raw b8edfd9731 subscribe to script hash updates for new mempool txes 2020-07-27 11:29:32 +02:00
Craig Raw 7428d21cac minor hwi and mempool fetching tweaks 2020-07-27 08:15:15 +02:00
Craig Raw 1c03e1935e wallet tx usb signing 2020-07-26 14:03:24 +02:00
Craig Raw ef5124a9b9 psbt combining, transaction viewer updating 2020-07-24 14:47:43 +02:00
Craig Raw b0f4d3b4c9 minor fix 2020-07-24 10:00:17 +02:00
Craig Raw 7d3aabb050 signature count updating on signed keystores list change 2020-07-23 14:15:22 +02:00
Craig Raw 1fd1b28994 signature count updating on signed keystores list change 2020-07-23 14:14:47 +02:00
Craig Raw 33d6da8a99 cleanup psbt utxo fetching 2020-07-23 13:39:35 +02:00
Craig Raw 279b0e0ac1 sign psbt inputs 2020-07-22 14:56:59 +02:00
Craig Raw a188f70dbe tweaks and fixes 2020-07-21 08:52:21 +02:00
Craig Raw fca55779bf signatures progress bar and sign buttons 2020-07-20 17:05:08 +02:00
Craig Raw 6ef333ae2a finalize psbt tweaks 2020-07-17 17:37:51 +02:00
Craig Raw c86f7d32cb finalise tx with signing wallet and sighash 2020-07-17 17:24:52 +02:00
Craig Raw 8e02f7103e fix tx hex area segwit marker 2020-07-17 09:17:03 +02:00
Craig Raw 907062ffa0 create psbt tab from send 2020-07-16 15:16:43 +02:00
Craig Raw e79f5ba977 minor bug fix 2020-07-16 10:53:26 +02:00
Craig Raw d2291ef870 balancechart add interstitials 2020-07-16 10:48:00 +02:00
Craig Raw bd721be1a2 preferences update 2020-07-16 10:25:24 +02:00
Craig Raw 3cee45223e utxo grouping, filtering and zero conf handling 2020-07-15 09:15:50 +02:00
Craig Raw 4d51bb40c2 refactor utxo selector creation 2020-07-14 13:36:43 +02:00
Craig Raw e7b511fc74 bnb and knapsack selectors 2020-07-14 09:21:53 +02:00
Craig Raw 603f3036fa add bnb utxo selector 2020-07-13 14:28:24 +02:00
Craig Raw 0ffb6e377b dont send recipient amounts less than dust threshold 2020-07-11 11:40:28 +02:00
Craig Raw a37028b53f fiat amount on send 2020-07-11 10:03:11 +02:00
Craig Raw 9b43429de9 use static formatter 2020-07-10 18:35:17 +02:00
Craig Raw d6218f2a58 introduce currencyrate to hold fiat rate 2020-07-10 18:26:56 +02:00
Craig Raw c477d31d3d fiat currency support 2020-07-10 17:13:32 +02:00
Craig Raw 539013919b balance chart, transactions view refactor, tx viewer coin labels updating 2020-07-10 12:48:36 +02:00
Craig Raw d8c19ac0f8 user set bitcoin unit support 2020-07-09 14:35:34 +02:00
Craig Raw fc5bff9788 move bitcoinunit to drongo 2020-07-09 10:25:49 +02:00
Craig Raw e21dbf87ef rename amount controls to coin 2020-07-09 10:17:54 +02:00
Craig Raw 765a4f4c82 settings ux tweaks, addressable script types only 2020-07-08 15:56:59 +02:00
Craig Raw 6a5b346608 amount formatter, send final tweaks 2020-07-08 14:18:07 +02:00
Craig Raw 6ecebd9cf6 tx diagram to fixed size, coin control lock 2020-07-07 13:58:59 +02:00
Craig Raw c8e38de5aa send all utxos support 2020-07-07 07:43:01 +02:00
Craig Raw 3305d0630a utxo multiple selection, long confirmation indicator 2020-07-06 09:24:29 +02:00
Craig Raw 5860a6e49f validate too large fee 2020-07-05 15:23:33 +02:00
Craig Raw 66e558faee send pane improvements 2020-07-05 15:02:01 +02:00
Craig Raw 48e741733b tx creation support 2020-07-04 12:52:40 +02:00
Craig Raw 013ed89e98 send controller utxo selection 2020-07-03 11:01:09 +02:00
Craig Raw 571c515a46 send controller initial, fee rates support 2020-06-30 09:23:55 +02:00
Craig Raw 1d0b66c45a add unused glyph 2020-06-29 07:57:07 +02:00
Craig Raw 72d14c15a8 remove colons 2020-06-28 16:09:29 +02:00
Craig Raw 8aa9d15620 recieve fixes 2020-06-28 16:05:07 +02:00
Craig Raw d8c94504c3 utxo chart rewrite 2020-06-28 15:46:52 +02:00
Craig Raw 67d624f049 handle label event and update entries 2020-06-27 15:55:49 +02:00
Craig Raw 766afae9c1 utxo bar chart initial commit 2020-06-26 18:34:35 +02:00
Craig Raw 07458d3dff add duplicate address indicator 2020-06-26 14:29:16 +02:00
Craig Raw cd7fc06819 improve multithreaded behaviour 2020-06-26 13:34:39 +02:00
Craig Raw 7444a87d89 add wallet utxo pane 2020-06-26 12:07:19 +02:00
Craig Raw 183d0ded2f handle null label 2020-06-25 17:50:57 +02:00
Craig Raw ef2e2a1855 add comments 2020-06-25 16:44:55 +02:00
Craig Raw 829be40243 fix removing added txes 2020-06-25 15:22:21 +02:00
Craig Raw 0f89327f35 add note on address listing 2020-06-25 11:39:39 +02:00
Craig Raw 150f65e7bd wallet tx and addresses updating 2020-06-25 11:25:35 +02:00
Craig Raw d4d61b8d41 bug fixing on walletsettingsupdated 2020-06-23 16:22:01 +02:00
Craig Raw a8f16c15e0 wallet tx confirmation indicator and event refactor 2020-06-23 14:08:14 +02:00
Craig Raw 21f642bb5c wallet transactions pane and wallet change events 2020-06-21 17:15:46 +02:00
Craig Raw d33ccfb672 quit menu item 2020-06-19 18:17:39 +02:00
Craig Raw efba293564 address history updating 2020-06-19 17:57:07 +02:00
Craig Raw b1e8e25837 save stored block on event 2020-06-19 15:49:53 +02:00
Craig Raw 44be6e7203 improvements to fetching and settings 2020-06-19 15:36:17 +02:00
Craig Raw e93ec08ba7 minor fix 2020-06-18 13:27:02 +02:00
Craig Raw 800512ee5b fix bugs with txes with many outputs 2020-06-18 12:58:59 +02:00
Craig Raw 0cd97649cc tx viewer fix various 2020-06-17 12:45:46 +02:00
Craig Raw 9cd8a9b9ee finish perf improvements to tx viewer 2020-06-17 11:34:26 +02:00
Craig Raw 82ca9552bd transaction view perf improvements 2020-06-16 15:55:55 +02:00
Craig Raw 7cc330fde9 use view ordering to improve transaction pane perf 2020-06-16 12:11:06 +02:00
Craig Raw 7f03778ec7 transaction view pagination complete 2020-06-16 11:39:10 +02:00
Craig Raw 6b12888111 add layer of indirection to avoid copying tx data 2020-06-15 14:44:47 +02:00
Craig Raw ebc2f9442e transaction view paging support initial commit 2020-06-15 10:00:49 +02:00
Craig Raw 51b2608abc check if locking script is p2sh, fix output reference bug 2020-06-14 14:00:37 +02:00
Craig Raw 09c4a01ca4 handle server errors on retreiving wallet history 2020-06-14 11:25:25 +02:00
Craig Raw b1b4734094 fetch transaction output transactions, handle coinbase input 2020-06-14 10:36:24 +02:00
Craig Raw 86eb8b8294 transaction viewer block transaction fetch and display 2020-06-11 17:00:16 +02:00
Craig Raw a3a20a788d view blocktransaction and transaction input/output selection 2020-06-10 16:15:56 +02:00
Craig Raw 936d42e7fd address table improvements 2020-06-10 14:15:45 +02:00
Craig Raw 70c4eeaf5e address table with hash indexes 2020-06-09 16:10:49 +02:00
Craig Raw 9aa8b10898 handle new blocks 2020-06-09 11:21:34 +02:00
Craig Raw 50ef1c1a07 handle connection errors and incoming notifications 2020-06-09 09:59:36 +02:00
Craig Raw 3a65261326 mac application menu setup 2020-06-05 17:25:55 +02:00
Craig Raw c5bcefd331 fix broken hwi installs 2020-06-05 16:05:13 +02:00
Craig Raw f5a857317d add ping service 2020-06-05 14:48:36 +02:00
Craig Raw 1e250193fd add welcome and preferences dialogs 2020-06-05 13:29:43 +02:00
Craig Raw 6731823bef check if tab wallet on event 2020-06-02 15:39:56 +02:00
Craig Raw df1ed196be get dates from block header, handle reorgs better 2020-06-02 15:16:12 +02:00
Craig Raw e97f652769 blockchain to block rename for transaction in block 2020-06-02 11:05:23 +02:00
Craig Raw a6e340eef5 refactor transaction history storage 2020-06-02 09:21:35 +02:00
Craig Raw 0836273c8f parse transactions by wallet node 2020-06-01 10:32:00 +02:00
Craig Raw b1785f352b wallet save improvements and fixes 2020-05-30 13:15:56 +02:00
Craig Raw 7e8496915a refactor wallet node to non inner class 2020-05-30 11:57:58 +02:00
Craig Raw 5223fe8d8c make wallet node static inner class 2020-05-30 11:47:23 +02:00
Craig Raw 331b83f7a0 fetch transaction history 2020-05-30 11:22:51 +02:00
Craig Raw c115f6e729 receive and address pane improvements 2020-05-28 11:42:42 +02:00
Craig Raw 3db7fc1e99 receive pane 2020-05-26 17:55:35 +02:00
Craig Raw cabd62166a addresses table 2020-05-26 10:19:34 +02:00
Craig Raw 3a8581da13 test fix, remove custom policy support 2020-05-23 16:54:37 +02:00
Craig Raw 6f2a0cd3c3 exporting fixes 2020-05-21 16:10:07 +02:00
Craig Raw 7fc0e9b530 add config, usb status fixes 2020-05-21 15:37:13 +02:00
Craig Raw fdd8327464 usb status, key progress timing 2020-05-21 11:01:59 +02:00
Craig Raw 2acc922b06 handle password key derivation in separate tasks 2020-05-19 16:42:46 +02:00
Craig Raw b52a7137c3 securely handle mnemonic seed in memory 2020-05-19 14:02:53 +02:00
Craig Raw 969771d377 use charsequence and securestring instead of string 2020-05-19 12:37:16 +02:00
Craig Raw 1ed59bb935 no fingerprint labels and tests 2020-05-18 17:19:43 +02:00
Craig Raw 68aeb5946a wallet encryption with argon2 key derivation 2020-05-18 15:50:46 +02:00
Craig Raw ae01fe9ee6 refactor keycrypters 2020-05-17 12:52:14 +02:00
Craig Raw 6b09dc0293 passphrase not stored 2020-05-16 15:09:47 +02:00
Craig Raw 1bf8c85a65 electrum seed version system support 2020-05-15 17:56:45 +02:00
Craig Raw d0e5da0ec8 wallet export, settings fixes 2020-05-15 12:56:38 +02:00
Craig Raw bb2ec1882d invalidate if derivation matches another script type 2020-05-14 13:33:55 +02:00
Craig Raw 02258dea8d refactor and cleanup importing 2020-05-13 18:15:46 +02:00
Craig Raw d516eaa9d6 refactor import accordion, add wallet import 2020-05-13 17:16:52 +02:00
Craig Raw bc5690346c finalising wallet settings 2020-05-13 12:47:21 +02:00
Craig Raw 84888b4a43 refactor to prepare for keystore encryption 2020-05-12 14:17:19 +02:00
Craig Raw d7c6f5d587 keycrypter changes 2020-05-11 17:40:19 +02:00
Craig Raw 9241f4381f keystore source quick chooser 2020-05-11 11:38:07 +02:00
Craig Raw 82a113afc3 generate new bip39 word list 2020-05-10 16:26:15 +02:00
Craig Raw 6872e069a9 integrate keystore importer 2020-05-09 14:17:48 +02:00
Craig Raw 9af17bcac8 use onedir packaging of hwi 2020-05-09 12:05:43 +02:00
Craig Raw ef5399008f hw improvements including arbitrary derivation 2020-05-08 16:08:15 +02:00
Craig Raw a409c28b20 bip39 keystore import 2020-05-08 14:37:19 +02:00
Craig Raw 910dfcdeb1 bip39 keystore import 2020-05-07 14:36:37 +02:00
Craig Raw 2de90dfdc1 coldcard singlesig new feature, private key handling 2020-05-06 17:14:12 +02:00
Craig Raw 6b651ec63a minor import cleanup 2020-05-05 09:27:22 +02:00
Craig Raw 2ee9f3d10a encrypted wallet and keystore import 2020-04-30 16:14:49 +02:00
Craig Raw 60c1c17d26 storage refactor and test 2020-04-30 13:41:17 +02:00
Craig Raw 981b379615 ecies input and output streams 2020-04-30 12:14:25 +02:00
Craig Raw 14aa328f6d refactor .storage and .external to .io 2020-04-30 09:27:14 +02:00
Craig Raw 7a554df619 software wallet import and airgapped fixes 2020-04-29 16:51:33 +02:00
Craig Raw ae03da257a ensure keystore source and wallet model is always filled in 2020-04-29 15:44:18 +02:00
Craig Raw 728b6efbdf import airgapped keystores 2020-04-29 15:04:44 +02:00
Craig Raw 40d31ff025 usb keystore importing #2 2020-04-29 12:12:30 +02:00
Craig Raw 9adfcf5806 usb keystore importing 2020-04-29 12:03:44 +02:00
Craig Raw 431a170ab0 Merge branch 'master' of github.com:sparrowwallet/sparrow 2020-04-25 11:48:35 +02:00
Craig Raw 6d202f1522 wallet import and export - coldcard and electrum 2020-04-25 11:39:29 +02:00
Craig Raw 94eca5a83d wallet import and export - coldcard and electrum 2020-04-25 11:39:29 +02:00
Craig Raw 98b1aa0b1d wallet load and save 2020-04-23 13:41:43 +02:00
Craig Raw fb621963dd wallet load and save 2020-04-23 13:41:43 +02:00
Craig Raw 1f93d574eb add mit license 2020-04-20 10:52:57 +02:00
Craig Raw 72511fb184 wallet load, revert and save 2020-04-19 19:07:46 +02:00
Craig Raw e717589c9f refactor extpubkey 2020-04-18 12:39:11 +02:00
Craig Raw 8da6226545 wallet settings and keystores 2020-04-18 10:43:32 +02:00
Craig Raw 17b03a6750 wallet and settings initial work 2020-04-17 10:03:50 +02:00
Craig Raw 950ae07df5 sign and notarise app for osx 2020-04-14 09:37:14 +02:00
334 changed files with 22744 additions and 558 deletions
+3 -1
View File
@@ -4,4 +4,6 @@
build
/*.properties
out
*.log
*.log
build-*.sh
.DS_Store
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2020 Craig Raw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
BSD-2-Clause Plus Patent License
SPDX-License-Identifier: [BSD-2-Clause-Patent](https://spdx.org/licenses/BSD-2-Clause-Patent.html)
Copyright © 2020 Blockchain Commons, LLC
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or
(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise.
DISCLAIMER
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+63 -10
View File
@@ -12,11 +12,13 @@ sourceCompatibility = 1.9
repositories {
mavenCentral()
maven { url 'https://oss.sonatype.org/content/groups/public' }
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
}
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml' ]
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
java {
@@ -29,9 +31,21 @@ dependencies {
exclude group: 'junit'
}
implementation('com.google.guava:guava:28.2-jre')
implementation('com.google.code.gson:gson:2.8.6')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('org.controlsfx:controlsfx:11.0.1' ) {
implementation('com.google.zxing:javase:3.4.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0')
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20200803')
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@@ -45,29 +59,67 @@ dependencies {
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
compileJava {
options.with {
fork = true
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"])
}
}
run {
applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls"]
applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
"--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls",
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
jlink {
mergedModule {
requires 'javafx.graphics';
requires 'javafx.controls';
requires 'java.xml';
requires 'javafx.graphics'
requires 'javafx.controls'
requires 'java.xml'
requires 'java.logging'
requires 'javafx.base';
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls"]
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
"--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls",
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "0.5"
appVersion = "0.9.1"
skipInstaller = true
imageOptions = []
installerOptions = [
'--file-associations', 'src/main/resources/associations.properties',
@@ -76,7 +128,8 @@ jlink {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
}
if (org.gradle.internal.os.OperatingSystem.current().macOsX) {
imageOptions += ['--icon', 'src/main/resources/sparrow.icns']
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/macosx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macosx/']
installerType = "dmg"
}
}
+1 -1
Submodule drongo updated: 0cebee3d22...446eac3483
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleExecutable</key>
<string>Sparrow</string>
<key>CFBundleIconFile</key>
<string>Sparrow.icns</string>
<key>CFBundleIdentifier</key>
<string>com.sparrowwallet.sparrow</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Sparrow</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
<key>LSApplicationCategoryType</key>
<string>Unknown</string>
<key>CFBundleVersion</key>
<string>100</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C) 2020</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string>
</dict>
</plist>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.stage.Stage;
public class AboutController {
private Stage stage;
public void setStage(Stage stage) {
this.stage = stage;
}
public void close(ActionEvent event) {
stage.close();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.control.ScriptArea;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
import javafx.stage.Popup;
import org.fxmisc.richtext.event.MouseOverTextEvent;
import org.fxmisc.richtext.model.TwoDimensional;
import java.time.Duration;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
public abstract class BaseController {
protected void initializeScriptField(ScriptArea scriptArea) {
Popup popup = new Popup();
Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg);
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) {
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
if(!hoverChunk.isOpCode()) {
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeScriptChunk(hoverChunk));
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
}
}
});
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide();
});
}
protected String describeScriptChunk(ScriptChunk chunk) {
return chunk.toString();
}
}
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow;
import java.util.Currency;
public class CurrencyRate {
private final Currency currency;
private final Double btcRate;
public CurrencyRate(Currency currency, Double btcRate) {
this.currency = currency;
this.btcRate = btcRate;
}
public Currency getCurrency() {
return currency;
}
public boolean isAvailable() {
return btcRate != null && btcRate > 0.0;
}
public Double getBtcRate() {
return btcRate;
}
}
@@ -1,19 +1,57 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.sparrow.control.WelcomeDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import java.io.File;
import java.util.List;
import java.util.Optional;
public class MainApp extends Application {
public static final String APP_NAME = "Sparrow";
private Stage mainStage;
@Override
public void start(Stage stage) throws Exception {
this.mainStage = stage;
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
boolean createNewWallet = false;
Mode mode = Config.get().getMode();
if(mode == null) {
WelcomeDialog welcomeDialog = new WelcomeDialog(getHostServices());
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
}
}
}
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
Parent root = transactionLoader.load();
AppController appController = transactionLoader.getController();
appController.setApplication(this);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm());
@@ -22,11 +60,29 @@ public class MainApp extends Application {
stage.setMinWidth(650);
stage.setMinHeight(700);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/sparrow.png")));
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
appController.initializeView();
stage.show();
if(createNewWallet) {
appController.newWallet(null);
}
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
for(File walletFile : recentWalletFiles) {
if(walletFile.exists()) {
Platform.runLater(() -> appController.openWalletFile(walletFile));
}
}
}
}
@Override
public void stop() throws Exception {
mainStage.close();
}
public static void main(String[] args) {
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Mode {
OFFLINE, ONLINE
}
@@ -32,6 +32,6 @@ public class TabData {
}
public enum TabType {
TRANSACTION
WALLET, TRANSACTION
}
}
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.transaction.TransactionData;
public class TransactionTabData extends TabData {
private final TransactionData transactionData;
public TransactionTabData(TabType type, TransactionData transactionData) {
super(type);
this.transactionData = transactionData;
}
public TransactionData getTransactionData() {
return transactionData;
}
public Transaction getTransaction() {
return transactionData.getTransaction();
}
public PSBT getPsbt() {
return transactionData.getPsbt();
}
}
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.WalletForm;
public class WalletTabData extends TabData {
private final WalletForm walletForm;
public WalletTabData(TabType type, WalletForm walletForm) {
super(type);
this.walletForm = walletForm;
}
public WalletForm getWalletForm() {
return walletForm;
}
public Wallet getWallet() {
return walletForm.getWallet();
}
public Storage getStorage() {
return walletForm.getStorage();
}
}
@@ -0,0 +1,70 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, Entry> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
EntryCell.applyRowStyles(this, entry);
getStyleClass().add("address-cell");
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
setTooltip(tooltip);
if(utxoEntry.isDuplicateAddress()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
setGraphic(getDuplicateGlyph());
Tooltip tt = new Tooltip();
tt.setText(getTooltipText(utxoEntry));
setTooltip(tt);
} else {
setGraphic(null);
}
});
}
}
}
private String getTooltipText(UtxoEntry utxoEntry) {
return utxoEntry.getNode().getDerivationPath() + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
Glyph duplicateGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
duplicateGlyph.getStyleClass().add("duplicate-warning");
duplicateGlyph.setFontSize(12);
return duplicateGlyph;
}
}
@@ -0,0 +1,120 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Font;
import java.util.List;
import java.util.Optional;
public class AddressTreeTable extends CoinTreeTable {
public void initialize(NodeEntry rootEntry) {
getStyleClass().add("address-treetable");
setBitcoinUnit(rootEntry.getWallet());
String address = rootEntry.getAddress().toString();
updateAll(rootEntry);
setShowRoot(false);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address / Outpoints");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new EntryCell());
addressCol.setSortable(false);
getColumns().add(addressCol);
if(address != null) {
addressCol.setMinWidth(TextUtils.computeTextWidth(Font.font("Courier"), address, 0.0));
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new LabelCell());
labelCol.setSortable(false);
getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new CoinCell());
amountCol.setSortable(false);
getColumns().add(amountCol);
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(addressCol);
Integer highestUsedIndex = rootEntry.getNode().getHighestUsedIndex();
if(highestUsedIndex != null) {
scrollTo(highestUsedIndex);
}
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
}
}
}
}
});
}
public void updateAll(NodeEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
if(getColumns().size() > 0 && getSortOrder().isEmpty()) {
TreeTableColumn<Entry, ?> addressCol = getColumns().get(0);
getSortOrder().add(addressCol);
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
//We only ever add or replace child nodes - never remove in order to keep a full sequence
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
for(WalletNode updatedNode : updatedNodes) {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
if(optEntry.isPresent()) {
int index = rootEntry.getChildren().indexOf(optEntry.get());
rootEntry.getChildren().set(index, nodeEntry);
} else {
rootEntry.getChildren().add(nodeEntry);
}
}
sort();
}
public void updateLabel(Entry entry) {
Entry rootEntry = getRoot().getValue();
rootEntry.updateLabel(entry);
}
}
@@ -0,0 +1,102 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class BalanceChart extends LineChart<Number, Number> {
private XYChart.Series<Number, Number> balanceSeries;
private TransactionEntry selectedEntry;
public BalanceChart(@NamedArg("xAxis") Axis<Number> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize(WalletTransactionsEntry walletTransactionsEntry) {
managedProperty().bind(visibleProperty());
balanceSeries = new XYChart.Series<>();
getData().add(balanceSeries);
update(walletTransactionsEntry);
BitcoinUnit unit = Config.get().getBitcoinUnit();
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
}
public void update(WalletTransactionsEntry walletTransactionsEntry) {
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
balanceSeries.getData().clear();
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
.map(entry -> (TransactionEntry)entry)
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
.collect(Collectors.toList());
int size = balanceDataList.size() * 2;
for(int i = 0; i < size; i+= 2) {
Data<Number, Number> data = balanceDataList.get(i);
if(i + 1 < balanceDataList.size()) {
Data<Number, Number> nextData = balanceDataList.get(i + 1);
Data<Number, Number> interstitialData = new Data<>(nextData.getXValue(), data.getYValue(), null);
balanceDataList.add(i + 1, interstitialData);
} else {
Date now = new Date();
Data<Number, Number> interstitialData = new Data<>(now.getTime(), data.getYValue(), null);
balanceDataList.add(interstitialData);
}
}
if(!balanceDataList.isEmpty()) {
long min = balanceDataList.stream().map(data -> data.getXValue().longValue()).min(Long::compare).get();
long max = balanceDataList.stream().map(data -> data.getXValue().longValue()).max(Long::compare).get();
DateAxisFormatter dateAxisFormatter = new DateAxisFormatter(max - min);
NumberAxis xAxis = (NumberAxis)getXAxis();
xAxis.setTickLabelFormatter(dateAxisFormatter);
}
balanceSeries.getData().addAll(balanceDataList);
if(selectedEntry != null) {
select(selectedEntry);
}
}
public void select(TransactionEntry transactionEntry) {
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
for(Node selectedSymbol : selectedSymbols) {
selectedSymbol.getStyleClass().remove("selected");
}
for(int i = 0; i < balanceSeries.getData().size(); i++) {
XYChart.Data<Number, Number> data = balanceSeries.getData().get(i);
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
if(data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
symbol.getStyleClass().add("selected");
selectedEntry = transactionEntry;
}
}
}
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
}
}
@@ -0,0 +1,31 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import javafx.scene.chart.NumberAxis;
import javafx.util.StringConverter;
import java.text.ParseException;
final class CoinAxisFormatter extends StringConverter<Number> {
private final BitcoinUnit bitcoinUnit;
public CoinAxisFormatter(NumberAxis axis, BitcoinUnit unit) {
this.bitcoinUnit = unit;
}
@Override
public String toString(Number object) {
Double value = bitcoinUnit.getValue(object.longValue());
return CoinTextFormatter.COIN_FORMAT.format(value);
}
@Override
public Number fromString(String string) {
try {
Number number = CoinTextFormatter.COIN_FORMAT.parse(string);
return bitcoinUnit.getSatsValue(number.doubleValue());
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,85 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
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.scene.layout.Region;
import java.util.Locale;
class CoinCell extends TreeTableCell<Entry, Number> {
private final Tooltip tooltip;
public CoinCell() {
super();
tooltip = new Tooltip();
getStyleClass().add("coin-cell");
}
@Override
protected void updateItem(Number amount, boolean empty) {
super.updateItem(amount, empty);
if(empty || amount == null) {
setText(null);
setGraphic(null);
setTooltip(null);
} else {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
setText(btcValue);
} else {
tooltip.setText(btcValue + " " + BitcoinUnit.BTC.getLabel());
setText(satsValue);
}
setTooltip(tooltip);
String tooltipValue = tooltip.getText();
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(transactionEntry.isConfirming()) {
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
arc.confirmationsProperty().bind(transactionEntry.confirmationsProperty());
setGraphic(arc);
setContentDisplay(ContentDisplay.LEFT);
} else {
setGraphic(null);
}
} else if(entry instanceof UtxoEntry) {
setGraphic(null);
} else if(entry instanceof HashIndexEntry) {
Region node = new Region();
node.setPrefWidth(10);
setGraphic(node);
setContentDisplay(ContentDisplay.RIGHT);
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
satsValue = "-" + satsValue;
}
} else {
setGraphic(null);
}
}
}
}
@@ -1,6 +1,8 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.scene.control.ContextMenu;
@@ -14,13 +16,11 @@ import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class CoinLabel extends CopyableLabel {
public static final int MAX_SATS_SHOWN = 1000000;
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty value = new SimpleLongProperty();
private Tooltip tooltip;
private CoinContextMenu contextMenu;
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final Tooltip tooltip;
private final CoinContextMenu contextMenu;
public CoinLabel() {
this("Unknown");
@@ -28,31 +28,45 @@ public class CoinLabel extends CopyableLabel {
public CoinLabel(String text) {
super(text);
btcFormat.setMaximumFractionDigits(8);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
BTC_FORMAT.setMaximumFractionDigits(8);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
public final LongProperty valueProperty() {
return value;
return valueProperty;
}
public final long getValue() {
return value.get();
return valueProperty.get();
}
public final void setValue(long value) {
this.value.set(value);
this.valueProperty.set(value);
}
private void setValueAsText(Long value) {
public void refresh() {
refresh(Config.get().getBitcoinUnit());
}
public void refresh(BitcoinUnit bitcoinUnit) {
setValueAsText(getValue(), bitcoinUnit);
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats";
String btcValue = btcFormat.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
if(value > MAX_SATS_SHOWN) {
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
BitcoinUnit unit = bitcoinUnit;
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue);
setText(btcValue);
} else {
@@ -62,11 +76,8 @@ public class CoinLabel extends CopyableLabel {
}
private class CoinContextMenu extends ContextMenu {
private MenuItem copySatsValue;
private MenuItem copyBtcValue;
public CoinContextMenu() {
copySatsValue = new MenuItem("Copy Value in Satoshis");
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
copySatsValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
@@ -74,15 +85,20 @@ public class CoinLabel extends CopyableLabel {
Clipboard.getSystemClipboard().setContent(content);
});
copyBtcValue = new MenuItem("Copy Value in BTC");
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(btcFormat.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
}
}
public static DecimalFormat getBTCFormat() {
BTC_FORMAT.setMaximumFractionDigits(8);
return BTC_FORMAT;
}
}
@@ -0,0 +1,96 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########");
public CoinTextFormatter() {
super(new CoinFilter());
}
private static class CoinFilter implements UnaryOperator<Change> {
@Override
public Change apply(Change change) {
String oldText = change.getControlText();
String newText = change.getControlNewText();
String deleted = null;
if(change.isDeleted()) {
deleted = oldText.substring(change.getRangeStart(), change.getRangeEnd());
}
String noFractionCommaText = newText;
int commasRemoved = 0;
int dotIndex = newText.indexOf(".");
if(dotIndex > -1) {
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(",", "");
commasRemoved = newText.length() - noFractionCommaText.length();
}
if(!COIN_VALIDATION.matcher(noFractionCommaText).matches()) {
return null;
}
if(",".equals(change.getText())) {
return null;
}
if("".equals(newText)) {
return change;
}
if(change.isDeleted() && ",".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());
String compare = newText;
if(compare.contains(".") && compare.endsWith("0")) {
compare = compare.replaceAll("0*$", "");
}
if(compare.endsWith(".")) {
compare = compare.substring(0, compare.length() - 1);
}
if(correct.equals(compare)) {
return change;
}
if(value.doubleValue() == 0.0 && "0".equals(correct)) {
return change;
}
TextInputControl control = (TextInputControl)change.getControl();
change.setText(correct);
change.setRange(0, control.getLength());
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 caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
if(caret >= 0) {
change.setCaretPosition(caret);
change.setAnchor(caret);
}
}
return change;
} catch (ParseException e) {
return null;
}
}
}
}
@@ -0,0 +1,36 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.scene.control.TreeTableView;
public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit;
public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit;
}
public void setBitcoinUnit(BitcoinUnit bitcoinUnit) {
this.bitcoinUnit = bitcoinUnit;
}
public void setBitcoinUnit(Wallet wallet) {
setBitcoinUnit(wallet, Config.get().getBitcoinUnit());
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
boolean changed = (bitcoinUnit != unit);
this.bitcoinUnit = unit;
if(changed && !getChildren().isEmpty()) {
refresh();
}
}
}
@@ -0,0 +1,137 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import javafx.animation.*;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.util.Duration;
public class ConfirmationProgressIndicator extends StackPane {
private final Group confirmationGroup;
private final Arc arc;
private final Line downTickLine;
private final Line upTickLine;
public ConfirmationProgressIndicator(int confirmations) {
Circle circle = new Circle(7, 7, 7);
circle.setFill(Color.TRANSPARENT);
circle.getStyleClass().add("confirmation-progress-circle");
arc = new Arc(7, 7, 7, 7, 90, getDegrees(confirmations));
arc.setType(ArcType.ROUND);
arc.getStyleClass().add("confirmation-progress-arc");
downTickLine = new Line(4, 8, 4, 8);
downTickLine.setStrokeWidth(1.0);
downTickLine.setOpacity(0);
downTickLine.getStyleClass().add("confirmation-progress-tick");
upTickLine = new Line(6, 10, 6, 10);
upTickLine.setStrokeWidth(1.0);
upTickLine.setOpacity(0);
upTickLine.getStyleClass().add("confirmation-progress-tick");
confirmationGroup = new Group(circle, arc, downTickLine, upTickLine);
getStyleClass().add("confirmation-progress");
setAlignment(Pos.CENTER);
getChildren().addAll(confirmationGroup);
confirmationsProperty().set(confirmations);
confirmationsProperty().addListener((observable, oldValue, newValue) -> {
if(!oldValue.equals(newValue)) {
SequentialTransition sequence = new SequentialTransition();
Timeline arcLengthTimeline = new Timeline();
KeyValue arcLengthValue = new KeyValue(arc.lengthProperty(), getDegrees(newValue.intValue()));
KeyFrame arcLengthFrame = new KeyFrame(Duration.millis(3000), arcLengthValue);
arcLengthTimeline.getKeyFrames().add(arcLengthFrame);
sequence.getChildren().add(arcLengthTimeline);
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
Timeline arcRadiusTimeline = new Timeline();
KeyValue arcRadiusXValue = new KeyValue(arc.radiusXProperty(), 0.0);
KeyValue arcRadiusYValue = new KeyValue(arc.radiusYProperty(), 0.0);
KeyFrame arcRadiusFrame = new KeyFrame(Duration.millis(500), arcRadiusXValue, arcRadiusYValue);
arcRadiusTimeline.getKeyFrames().add(arcRadiusFrame);
sequence.getChildren().add(arcRadiusTimeline);
FadeTransition downTickFadeIn = new FadeTransition(Duration.millis(50), downTickLine);
downTickFadeIn.setFromValue(0);
downTickFadeIn.setToValue(1);
sequence.getChildren().add(downTickFadeIn);
Timeline downTickLineTimeline = new Timeline();
KeyValue downTickLineX = new KeyValue(downTickLine.endXProperty(), 6);
KeyValue downTickLineY = new KeyValue(downTickLine.endYProperty(), 10);
KeyFrame downTickLineFrame = new KeyFrame(Duration.millis(125), downTickLineX, downTickLineY);
downTickLineTimeline.getKeyFrames().add(downTickLineFrame);
sequence.getChildren().add(downTickLineTimeline);
FadeTransition upTickFadeIn = new FadeTransition(Duration.millis(50), upTickLine);
upTickFadeIn.setFromValue(0);
upTickFadeIn.setToValue(1);
sequence.getChildren().add(upTickFadeIn);
Timeline upTickLineTimeline = new Timeline();
KeyValue upTickLineX = new KeyValue(upTickLine.endXProperty(), 10);
KeyValue upTickLineY = new KeyValue(upTickLine.endYProperty(), 4);
KeyFrame upTickLineFrame = new KeyFrame(Duration.millis(250), upTickLineX, upTickLineY);
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
sequence.getChildren().add(upTickLineTimeline);
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(10), confirmationGroup);
groupFadeOut.setFromValue(1);
groupFadeOut.setToValue(0);
sequence.getChildren().add(groupFadeOut);
}
sequence.play();
}
});
}
private static double getDegrees(int confirmations) {
int requiredConfirmations = BlockTransactionHash.BLOCKS_TO_CONFIRM;
return ((double)Math.min(confirmations, requiredConfirmations)/ requiredConfirmations) * -360d;
}
/**
* Defines the number of confirmations
*/
private IntegerProperty confirmations;
public final void setConfirmations(int value) {
if(confirmations != null || value != 0) {
confirmationsProperty().set(value);
}
}
public final int getConfirmations() {
return confirmations == null ? 0 : confirmations.get();
}
public final IntegerProperty confirmationsProperty() {
if(confirmations == null) {
confirmations = new IntegerPropertyBase(0) {
@Override
public Object getBean() {
return ConfirmationProgressIndicator.this;
}
@Override
public String getName() {
return "confirmations";
}
};
}
return confirmations;
}
}
@@ -0,0 +1,64 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.FadeTransition;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.control.textfield.CustomTextField;
public class CopyableTextField extends CustomTextField {
private static final Duration FADE_DURATION = Duration.millis(350);
public CopyableTextField() {
super();
getStyleClass().add("copyable-text-field");
setupCopyButtonField(super.rightProperty());
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
Region copyButton = new Region();
copyButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
StackPane copyButtonPane = new StackPane(copyButton);
copyButtonPane.getStyleClass().addAll("copy-button"); //$NON-NLS-1$
copyButtonPane.setOpacity(0.0);
copyButtonPane.setCursor(Cursor.DEFAULT);
copyButtonPane.setOnMouseReleased(e -> {
ClipboardContent content = new ClipboardContent();
content.putString(getText());
Clipboard.getSystemClipboard().setContent(content);
});
rightProperty.set(copyButtonPane);
final FadeTransition fader = new FadeTransition(FADE_DURATION, copyButtonPane);
fader.setCycleCount(1);
textProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable arg0) {
String text = getText();
boolean isTextEmpty = text == null || text.isEmpty();
boolean isButtonVisible = fader.getNode().getOpacity() > 0;
if (isTextEmpty && isButtonVisible) {
setButtonVisible(false);
} else if (!isTextEmpty && !isButtonVisible) {
setButtonVisible(true);
}
}
private void setButtonVisible( boolean visible ) {
fader.setFromValue(visible? 0.0: 1.0);
fader.setToValue(visible? 1.0: 0.0);
fader.play();
}
});
}
}
@@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.control;
import javafx.util.StringConverter;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateAxisFormatter extends StringConverter<Number> {
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
private final DateFormat dateFormat;
private int oddCounter;
public DateAxisFormatter(long duration) {
if(duration < (24 * 60 * 60 * 1000L)) {
dateFormat = HOUR_FORMAT;
} else if(duration < (365 * 24 * 60 * 60 * 1000L)) {
dateFormat = DAY_FORMAT;
} else {
dateFormat = MONTH_FORMAT;
}
}
@Override
public String toString(Number object) {
oddCounter++;
return oddCounter % 3 == 0 ? dateFormat.format(new Date(object.longValue())) : "";
}
@Override
public Number fromString(String string) {
try {
Date date = dateFormat.parse(string);
return date.getTime();
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,69 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateCell extends TreeTableCell<Entry, Entry> {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public DateCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("date-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
EntryCell.applyRowStyles(this, entry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
Tooltip tooltip = new Tooltip();
int height = utxoEntry.getHashIndex().getHeight();
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
setTooltip(tooltip);
}
setGraphic(null);
}
}
private static class DateContextMenu extends ContextMenu {
public DateContextMenu(String date, BlockTransactionHashIndex reference) {
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(date);
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyHeight.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(reference.getHeight() > 0 ? Integer.toString(reference.getHeight()) : "Mempool");
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyDate, copyHeight);
}
}
}
@@ -0,0 +1,31 @@
package com.sparrowwallet.sparrow.control;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateLabel extends CopyableLabel {
private static final DateFormat LAST_24_HR = new SimpleDateFormat("HH:mm");
private static final DateFormat LAST_YEAR = new SimpleDateFormat("d MMM");
private static final DateFormat ALL_TIME = new SimpleDateFormat("yyyy/MM/dd");
private Date date;
public DateLabel(Date date) {
super(getShortDateFormat(date));
this.date = date;
}
public static String getShortDateFormat(Date date) {
Date now = new Date();
long elapsed = (now.getTime() - date.getTime()) / 1000;
if(elapsed < 24 * 60 * 60) {
return LAST_24_HR.format(date);
} else if(elapsed < 365 * 24 * 60 * 60) {
return LAST_YEAR.format(date);
} else {
return ALL_TIME.format(date);
}
}
}
@@ -0,0 +1,36 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final KeyDerivation keyDerivation;
public DeviceAddressDialog(List<Device> devices, Wallet wallet, KeyDerivation keyDerivation) {
super(devices);
this.wallet = wallet;
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, keyDerivation, device);
}
@Subscribe
public void addressDisplayed(AddressDisplayedEvent event) {
setResult(event.getAddress());
}
}
@@ -0,0 +1,126 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.glyphfont.Glyph;
import java.util.List;
public abstract class DeviceDialog<R> extends Dialog<R> {
private final List<Device> operationDevices;
private final Accordion deviceAccordion;
private final VBox scanBox;
private final Label scanLabel;
public DeviceDialog() {
this(null);
}
public DeviceDialog(List<Device> operationDevices) {
this.operationDevices = operationDevices;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
deviceAccordion = new Accordion();
scrollPane.setContent(deviceAccordion);
scanBox = new VBox();
scanBox.setSpacing(30);
scanBox.setAlignment(Pos.CENTER);
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
usb.setFontSize(50);
scanLabel = new Label("Connect Hardware Wallet");
Button button = new Button("Scan...");
button.setPrefSize(120, 60);
button.setOnAction(event -> {
scan();
});
scanBox.getChildren().addAll(usb, scanLabel, button);
scanBox.managedProperty().bind(scanBox.visibleProperty());
stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppController.getDevices();
if(devices == null || devices.isEmpty()) {
scanBox.setVisible(true);
} else {
Platform.runLater(() -> setDevices(devices));
scanBox.setVisible(false);
}
final ButtonType rescanButtonType = new javafx.scene.control.ButtonType("Rescan", ButtonBar.ButtonData.NO);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(rescanButtonType, cancelButtonType);
Button rescanButton = (Button) dialogPane.lookupButton(rescanButtonType);
rescanButton.managedProperty().bind(rescanButton.visibleProperty());
rescanButton.visibleProperty().bind(scanBox.visibleProperty().not());
rescanButton.addEventFilter(ActionEvent.ACTION, event -> {
scan();
event.consume();
});
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
}
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
setDevices(devices);
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
enumerateService.start();
}
protected void setDevices(List<Device> devices) {
List<Device> dialogDevices = devices;
if(operationDevices != null && dialogDevices.containsAll(operationDevices)) {
dialogDevices = operationDevices;
}
deviceAccordion.getPanes().clear();
if(dialogDevices.isEmpty()) {
scanBox.setVisible(true);
scanLabel.setText("No devices found");
} else {
scanBox.setVisible(false);
for(Device device : dialogDevices) {
DevicePane devicePane = getDevicePane(device);
deviceAccordion.getPanes().add(devicePane);
}
}
}
protected abstract DevicePane getDevicePane(Device device);
}
@@ -0,0 +1,493 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.util.List;
public class DevicePane extends TitledDescriptionPane {
private final DeviceOperation deviceOperation;
private final Wallet wallet;
private final PSBT psbt;
private final KeyDerivation keyDerivation;
private final Device device;
private CustomPasswordField pinField;
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private SplitMenuButton importButton;
private Button signButton;
private Button displayAddressButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
public DevicePane(Wallet wallet, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = null;
this.device = device;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createImportButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
public DevicePane(PSBT psbt, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.psbt = psbt;
this.keyDerivation = null;
this.device = device;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createSignButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.device = device;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createDisplayAddressButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
@Override
protected Control createButton() {
createUnlockButton();
return unlockButton;
}
private void setDefaultStatus() {
setDescription(device.getNeedsPinSent() ? "Locked" : device.getNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
}
private void createUnlockButton() {
unlockButton = new Button("Unlock");
unlockButton.setAlignment(Pos.CENTER_RIGHT);
unlockButton.setOnAction(event -> {
unlockButton.setDisable(true);
unlock(device);
});
unlockButton.managedProperty().bind(unlockButton.visibleProperty());
unlockButton.setVisible(false);
}
private void createSetPassphraseButton() {
setPassphraseButton = new Button("Set Passphrase");
setPassphraseButton.setAlignment(Pos.CENTER_RIGHT);
setPassphraseButton.setOnAction(event -> {
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
});
setPassphraseButton.managedProperty().bind(setPassphraseButton.visibleProperty());
setPassphraseButton.setVisible(false);
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation());
});
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
for(int i = 0; i < accounts.length; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private void createSignButton() {
signButton = new Button("Sign");
signButton.setDefaultButton(true);
signButton.setAlignment(Pos.CENTER_RIGHT);
signButton.setMinWidth(44);
signButton.setOnAction(event -> {
signButton.setDisable(true);
sign();
});
signButton.managedProperty().bind(signButton.visibleProperty());
signButton.setVisible(false);
}
private void createDisplayAddressButton() {
displayAddressButton = new Button("Display Address");
displayAddressButton.setDefaultButton(true);
displayAddressButton.setAlignment(Pos.CENTER_RIGHT);
displayAddressButton.setOnAction(event -> {
displayAddressButton.setDisable(true);
displayAddress();
});
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
displayAddressButton.setDisable(true);
}
}
private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) {
promptPin();
}
}
private Node getPinEntry() {
VBox vBox = new VBox();
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
enterPinButton = new Button("Enter PIN");
enterPinButton.setOnAction(event -> {
enterPinButton.setDisable(true);
sendPin(pinField.getText());
});
vBox.getChildren().addAll(pinField, enterPinButton);
TilePane tilePane = new TilePane();
tilePane.setPrefColumns(3);
tilePane.setHgap(10);
tilePane.setVgap(10);
tilePane.setMaxWidth(150);
tilePane.setMaxHeight(120);
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
for(int i = 0; i < digits.length; i++) {
Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]);
tilePane.getChildren().add(pinButton);
pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData());
});
}
HBox contentBox = new HBox();
contentBox.setSpacing(50);
contentBox.getChildren().add(tilePane);
contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER);
return contentBox;
}
private Node getPassphraseEntry() {
CustomTextField passphraseField = (CustomTextField)TextFields.createClearableTextField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
Button sendPassphraseButton = new Button("Send Passphrase");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
sendPassphrase(passphrase.get());
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(passphraseField);
contentBox.getChildren().add(sendPassphraseButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
return contentBox;
}
private void promptPin() {
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
promptPinService.setOnSucceeded(workerStateEvent -> {
Boolean result = promptPinService.getValue();
if(result) {
setContent(getPinEntry());
setExpanded(true);
} else {
setError("Could not request PIN", null);
unlockButton.setDisable(false);
}
});
promptPinService.setOnFailed(workerStateEvent -> {
setError(promptPinService.getException().getMessage(), null);
unlockButton.setDisable(false);
});
promptPinService.start();
}
private void sendPin(String pin) {
Hwi.SendPinService sendPinService = new Hwi.SendPinService(device, pin);
sendPinService.setOnSucceeded(workerStateEvent -> {
Boolean result = sendPinService.getValue();
if(result) {
device.setNeedsPinSent(false);
setDefaultStatus();
setExpanded(false);
unlockButton.setVisible(false);
if(device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
} else {
showOperationButton();
}
} else {
setError("Incorrect PIN", null);
unlockButton.setDisable(false);
if(pinField != null) {
pinField.setText("");
}
}
});
sendPinService.setOnFailed(workerStateEvent -> {
setError(sendPinService.getException().getMessage(), null);
enterPinButton.setDisable(false);
});
setDescription("Unlocking...");
showHideLink.setVisible(false);
sendPinService.start();
}
private void sendPassphrase(String passphrase) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase);
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
for (Device freshDevice : devices) {
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
device.setFingerprint(freshDevice.getFingerprint());
}
}
if(device.getFingerprint() != null) {
setPassphraseButton.setVisible(false);
device.setNeedsPassphraseSent(false);
setDefaultStatus();
showOperationButton();
} else {
setError("Passphrase send failed", null);
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
}
});
enumerateService.setOnFailed(workerStateEvent -> {
setError(enumerateService.getException().getMessage(), null);
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
});
enumerateService.start();
}
private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
for (Device freshDevice : devices) {
if (device.getPath().equals(freshDevice.getPath()) && device.getModel().equals(freshDevice.getModel())) {
device.setFingerprint(freshDevice.getFingerprint());
}
}
importXpub(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError(enumerateService.getException().getMessage(), null);
importButton.setDisable(false);
});
enumerateService.start();
} else {
importXpub(derivation);
}
}
private void importXpub(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
EventManager.get().post(new KeystoreImportEvent(keystore));
});
getXpubService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubService.getException().getMessage());
importButton.setDisable(false);
});
setDescription("Importing...");
showHideLink.setVisible(false);
getXpubService.start();
}
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));
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
signButton.setDisable(false);
});
setDescription("Signing...");
showHideLink.setVisible(false);
signPSBTService.start();
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath());
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
displayAddressButton.setDisable(false);
});
setDescription("Check device for address");
displayAddressService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setVisible(true);
showHideLink.setVisible(false);
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
Button importDerivationButton = new Button("Import");
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importXpub(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS;
}
}
@@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceSignDialog extends DeviceDialog<PSBT> {
private final PSBT psbt;
public DeviceSignDialog(List<Device> devices, PSBT psbt) {
super(devices);
this.psbt = psbt;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt);
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(psbt, device);
}
@Subscribe
public void psbtSigned(PSBTSignedEvent event) {
if(psbt == event.getPsbt()) {
setResult(event.getSignedPsbt());
}
}
}
@@ -0,0 +1,234 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.stream.Collectors;
class EntryCell extends TreeTableCell<Entry, Entry> {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public EntryCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("entry-cell");
}
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
applyRowStyles(this, entry);
if(empty) {
setText(null);
setGraphic(null);
} else {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip);
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
});
setGraphic(viewTransactionButton);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
setTooltip(tooltip);
getStyleClass().add("address-cell");
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
receiveButton.setGraphic(receiveGlyph);
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
HBox actionBox = new HBox();
Button viewTransactionButton = new Button("");
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
viewTransactionButton.setGraphic(searchGlyph);
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
actionBox.getChildren().add(viewTransactionButton);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) {
Button spendUtxoButton = new Button("");
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.setFontSize(12);
spendUtxoButton.setGraphic(sendGlyph);
spendUtxoButton.setOnAction(event -> {
List<HashIndexEntry> utxoEntries = getTreeTableView().getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent())
.collect(Collectors.toList());
if(!utxoEntries.contains(hashIndexEntry)) {
utxoEntries = List.of(hashIndexEntry);
}
final List<HashIndexEntry> spendingUtxoEntries = utxoEntries;
EventManager.get().post(new SendActionEvent(utxoEntries));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries)));
});
actionBox.getChildren().add(spendUtxoButton);
}
setGraphic(actionBox);
}
}
}
private static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(date);
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyHeight.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyDate, copyTxid, copyHeight);
}
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(address.toString());
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();
ClipboardContent content = new ClipboardContent();
content.putString(outputDescriptor);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
}
}
private static class HashIndexEntryContextMenu extends ContextMenu {
public HashIndexEntryContextMenu(HashIndexEntry hashIndexEntry) {
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
}
}
public static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("spent");
if(entry != null) {
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");
}
});
}
} else if(entry instanceof NodeEntry) {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry) {
cell.getStyleClass().add("utxo-row");
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
}
}
}
}
@@ -0,0 +1,57 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.XYChart;
import java.util.Iterator;
import java.util.Map;
public class FeeRatesChart extends LineChart<String, Number> {
private XYChart.Series<String, Number> feeRateSeries;
private Integer selectedTargetBlocks;
public FeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize() {
feeRateSeries = new XYChart.Series<>();
getData().add(feeRateSeries);
}
public void update(Map<Integer, Double> targetBlocksFeeRates) {
feeRateSeries.getData().clear();
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
Integer targetBlocks = targetBlocksIter.next();
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
}
if(selectedTargetBlocks != null) {
select(selectedTargetBlocks);
}
}
public void select(Integer targetBlocks) {
Node selectedSymbol = lookup(".chart-line-symbol.selected");
if(selectedSymbol != null) {
selectedSymbol.getStyleClass().remove("selected");
}
for(int i = 0; i < feeRateSeries.getData().size(); i++) {
XYChart.Data<String, Number> data = feeRateSeries.getData().get(i);
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
if(data.getXValue().replace("+", "").equals(targetBlocks.toString())) {
symbol.getStyleClass().add("selected");
selectedTargetBlocks = targetBlocks;
}
}
}
}
}
@@ -0,0 +1,124 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.CurrencyRate;
import javafx.beans.property.*;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.Currency;
public class FiatLabel extends CopyableLabel {
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00");
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
private final Tooltip tooltip;
private final FiatContextMenu contextMenu;
public FiatLabel() {
this("");
}
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()));
tooltip = new Tooltip();
contextMenu = new FiatContextMenu();
}
public final LongProperty valueProperty() {
return valueProperty;
}
public final long getValue() {
return valueProperty.get();
}
public final void setValue(long value) {
this.valueProperty.set(value);
}
public final DoubleProperty btcRateProperty() {
return btcRateProperty;
}
public final double getBtcRate() {
return btcRateProperty.get();
}
public final void setBtcRate(double btcRate) {
this.btcRateProperty.set(btcRate);
}
public final ObjectProperty<Currency> currencyProperty() {
return currencyProperty;
}
public final Currency getCurrency() {
return currencyProperty.get();
}
public final void setCurrency(Currency currency) {
this.currencyProperty.set(currency);
}
public final void set(CurrencyRate currencyRate, long value) {
set(currencyRate.getCurrency(), currencyRate.getBtcRate(), value);
}
public final void set(Currency currency, double btcRate, long value) {
setValue(value);
setBtcRate(btcRate);
setCurrency(currency);
}
private void setValueAsText(long balance) {
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()));
setText(label);
setTooltip(tooltip);
setContextMenu(contextMenu);
} else {
setText("");
setTooltip(null);
setContextMenu(null);
}
}
private class FiatContextMenu extends ContextMenu {
public FiatContextMenu() {
MenuItem copyValue = new MenuItem("Copy Value");
copyValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(getText());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyRate = new MenuItem("Copy Rate");
copyRate.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(getTooltip().getText());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyValue, copyRate);
}
}
}
@@ -0,0 +1,121 @@
package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.sparrow.io.FileImport;
import com.sparrowwallet.sparrow.io.ImportException;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public abstract class FileImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
private final FileImport importer;
private Button importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl) {
super(title, description, content, imageUrl);
this.importer = importer;
}
@Override
protected Control createButton() {
importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importFile();
});
return importButton;
}
private void importFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json")
);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
importFile(file, null);
}
}
private void importFile(File file, String password) {
if(file.exists()) {
try {
if(importer.isEncrypted(file) && password == null) {
setDescription("Password Required");
showHideLink.setVisible(false);
setContent(getPasswordEntry(file));
importButton.setDisable(true);
setExpanded(true);
} else {
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
importFile(file.getName(), inputStream, password);
}
} catch (Exception e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
if(e instanceof InvalidPasswordException || e.getCause() instanceof InvalidPasswordException) {
errorMessage = "Invalid wallet password";
}
if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) {
errorMessage = "File was not in JSON format";
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
}
}
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
passwordField.setPromptText("Wallet password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
Button importEncryptedButton = new Button("Import");
importEncryptedButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
importFile(file, password.get());
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(passwordField);
contentBox.getChildren().add(importEncryptedButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import java.io.*;
public class FileKeystoreImportPane extends FileImportPane {
protected final Wallet wallet;
private final KeystoreFileImport importer;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
super(importer, importer.getName(), "Keystore file import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
@@ -0,0 +1,95 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.WalletExport;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.Optional;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.exporter = exporter;
}
@Override
protected Control createButton() {
Button exportButton = new Button("Export Wallet...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportWallet();
});
return exportButton;
}
private void exportWallet() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
File file = fileChooser.showSaveDialog(window);
if(file != null) {
exportWallet(file);
}
}
private void exportWallet(File file) {
Wallet copy = wallet.copy();
if(copy.isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
exportWallet(file, decryptedWallet);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed"));
setError("Export Error", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
exportWallet(file, copy);
}
}
private void exportWallet(File file, Wallet decryptedWallet) {
try {
OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(decryptedWallet, outputStream);
EventManager.get().post(new WalletExportEvent(decryptedWallet));
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
} finally {
decryptedWallet.clearPrivate();
}
}
}
@@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.WalletImport;
import java.io.InputStream;
public class FileWalletImportPane extends FileImportPane {
private final WalletImport importer;
public FileWalletImportPane(WalletImport importer) {
super(importer, importer.getName(), "Wallet file import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.importer = importer;
}
@Override
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importer.importWallet(inputStream, password);
if(wallet.getName() == null) {
wallet.setName(fileName);
}
EventManager.get().post(new WalletImportEvent(wallet));
}
}
@@ -0,0 +1,45 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import org.controlsfx.glyphfont.Glyph;
public class HelpLabel extends Label {
private final Tooltip tooltip;
public HelpLabel() {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
setTooltip(tooltip);
getStyleClass().add("help-label");
}
private static Glyph getHelpGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
lockGlyph.getStyleClass().add("help-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
public final StringProperty helpTextProperty() {
if(helpText == null) {
helpText = new SimpleStringProperty(this, "helpText", "");
}
return helpText;
}
private StringProperty helpText;
public final void setHelpText(String value) {
helpTextProperty().setValue(value);
}
public final String getHelpText() {
return helpText == null ? "" : helpText.getValue();
}
}
@@ -0,0 +1,46 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppController;
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.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;
public KeystorePassphraseDialog(Keystore keystore) {
this(null, keystore);
}
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.KEY);
key.setFontSize(50);
dialogPane.setGraphic(key);
final VBox content = new VBox(10);
content.setPrefHeight(50);
content.getChildren().add(passphrase);
dialogPane.setContent(content);
Platform.runLater(passphrase::requestFocus);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
}
}
@@ -0,0 +1,95 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.event.Event;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.util.converter.DefaultStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
public LabelCell() {
super(new DefaultStringConverter());
getStyleClass().add("label-cell");
}
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
if(empty) {
setText(null);
setGraphic(null);
} else {
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
setText(label);
setContextMenu(new LabelContextMenu(label));
}
}
@Override
public void commitEdit(String label) {
if(label != null) {
label = label.trim();
}
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !label.equals(getItem())) {
TreeTableView<Entry> treeTable = getTreeTableView();
if(treeTable != null) {
TreeTableColumn<Entry, String> column = getTableColumn();
TreeTableColumn.CellEditEvent<Entry, String> event = new TreeTableColumn.CellEditEvent<>(
treeTable, new TreeTablePosition<>(treeTable, getIndex(), column),
TreeTableColumn.editCommitEvent(), label
);
Event.fireEvent(column, event);
}
}
super.commitEdit(label);
}
@Override
public void startEdit() {
super.startEdit();
try {
Field f = getClass().getSuperclass().getDeclaredField("textField");
f.setAccessible(true);
TextField textField = (TextField)f.get(this);
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused) {
commitEdit(getConverter().fromString(textField.getText()));
setText(getConverter().fromString(textField.getText()));
}
});
} catch (Exception e) {
log.error("Error starting edit", e);
}
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyLabel);
}
}
}
@@ -0,0 +1,486 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Callback;
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;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
private final KeystoreMnemonicImport importer;
private SplitMenuButton enterMnemonicButton;
private SplitMenuButton importButton;
private TilePane wordsPane;
private Button generateButton;
private Label validLabel;
private Label invalidLabel;
private Button calculateButton;
private Button backButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
private SimpleListProperty<String> wordEntriesProperty;
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Mnemonic import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
createImportButton();
buttonBox.getChildren().add(importButton);
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
buttonBox.getChildren().clear();
showWordList(keystore.getSeed());
}
@Override
protected Control createButton() {
createEnterMnemonicButton();
return enterMnemonicButton;
}
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Set Words Length");
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(24);
});
int[] numberWords = new int[] {24, 21, 18, 15, 12};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem(numberWords[i] + " words");
final int words = numberWords[i];
item.setOnAction(event -> {
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.getStyleClass().add("default-button");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
});
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
for(int i = 0; i < accounts.length; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation, false);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private void enterMnemonic(int numWords) {
generatedMnemonicCode = null;
setDescription("Enter mnemonic word list");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords, false));
setExpanded(true);
}
private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) {
VBox vBox = new VBox();
vBox.setSpacing(10);
wordsPane = new TilePane();
wordsPane.setPrefRows(numWords/3);
wordsPane.setHgap(10);
wordsPane.setVgap(10);
wordsPane.setOrientation(Orientation.VERTICAL);
List<String> words = new ArrayList<>();
for(int i = 0; i < numWords; i++) {
words.add("");
}
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
for(int i = 0; i < numWords; i++) {
WordEntry wordEntry = new WordEntry(i, wordEntryList);
wordsPane.getChildren().add(wordEntry);
}
vBox.getChildren().add(wordsPane);
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 26, 0, 10));
generateButton = new Button("Generate New");
generateButton.setOnAction(event -> {
generateNew();
});
generateButton.managedProperty().bind(generateButton.visibleProperty());
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
buttonPane.getChildren().add(generateButton);
AnchorPane.setLeftAnchor(generateButton, 0.0);
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
buttonPane.getChildren().add(confirmButton);
AnchorPane.setRightAnchor(confirmButton, 0.0);
calculateButton = new Button("Calculate Seed");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Calculate the seed from the provided word list"));
backButton = new Button("Back");
backButton.setOnAction(event -> {
displayMnemonicCode();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
boolean validChecksum = false;
for(String word : wordEntryList) {
if(!word.isEmpty()) {
empty = false;
}
if(!WordEntry.isValid(word)) {
validWords = false;
}
}
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
}
}
generateButton.setVisible(empty && generatedMnemonicCode == null);
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
});
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(backButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
}
return vBox;
}
private void generateNew() {
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
displayMnemonicCode();
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
confirmButton.setVisible(true);
backButton.setVisible(false);
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
throw new IllegalStateException("Generated mnemonic words list not same size as displayed words list");
}
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
wordEntry.getEditor().setEditable(false);
}
}
private void confirmBackup() {
setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
}
private void prepareImport() {
if(generatedMnemonicCode != null && !generatedMnemonicCode.equals(wordEntriesProperty.get())) {
setError("Import Error", "Confirmation words did not match generated mnemonic");
return;
}
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
setExpanded(true);
enterMnemonicButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
setDescription("Ready to import");
showHideLink.setText("Show Derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
}
}
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
} catch (ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
errorMessage = "Invalid word list - checksum incorrect";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
return false;
}
}
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
setAlignment(Pos.CENTER_RIGHT);
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField.setMaxWidth(100);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
wordEntryList.set(wordNumber, newValue);
});
this.getChildren().addAll(label, wordField);
}
public TextField getEditor() {
return wordField;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
private static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
private final List<String> wordList;
public WordlistSuggestionProvider(List<String> wordList) {
this.wordList = wordList;
}
@Override
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.equals(request.getUserText())) {
return Collections.emptyList();
}
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
}
}
return suggestions;
}
}
private class PassphraseEntry extends HBox {
public PassphraseEntry() {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label passphraseLabel = new Label("Passphrase:");
CustomTextField passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
HelpLabel helpLabel = new HelpLabel();
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!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
Button importDerivationButton = new Button("Import");
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKeystore(importDerivation, false);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
private void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size(), true));
setExpanded(true);
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("valid-checksum");
validGlyph.setFontSize(12);
return validGlyph;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("invalid-checksum");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}
@@ -0,0 +1,114 @@
package com.sparrowwallet.sparrow.control;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.UREncoder;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 100;
private final UR ur;
private final UREncoder encoder;
private final ImageView qrImageView;
private String currentPart;
public QRDisplayDialog(byte[] data) {
this(UR.fromBytes(data));
}
public QRDisplayDialog(UR ur) {
this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
nextPart();
if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
AnimateQRService animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(100));
animateQRService.start();
setOnCloseRequest(event -> {
animateQRService.cancel();
});
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
private void nextPart() {
String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase();
}
private Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, 480, 480);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
return new Image(bais);
} catch(Exception e) {
log.error("Error generating QR", e);
}
return null;
}
private class AnimateQRService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws ImportException {
Image qrImage = getQrCode(currentPart);
qrImageView.setImage(qrImage);
nextPart();
return true;
}
};
}
}
}
@@ -0,0 +1,231 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamResolution;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.sparrow.ur.ResultType;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.URDecoder;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.layout.StackPane;
import org.controlsfx.tools.Borders;
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private final URDecoder decoder;
private final WebcamService webcamService;
private boolean isUr;
private QRScanDialog.Result result;
public QRScanDialog() {
this.decoder = new URDecoder();
this.webcamService = new WebcamService(WebcamResolution.VGA);
WebcamView webcamView = new WebcamView(webcamService);
final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
});
webcamService.start();
setOnCloseRequest(event -> {
Platform.runLater(webcamService::cancel);
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(660);
dialogPane.setPrefHeight(550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
}
private class QRResultListener implements ChangeListener<com.google.zxing.Result> {
@Override
public void changed(ObservableValue<? extends com.google.zxing.Result> observable, com.google.zxing.Result oldValue, com.google.zxing.Result qrResult) {
if(result != null) {
Platform.runLater(() -> setResult(result));
}
//Try text first
String qrtext = qrResult.getText();
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
isUr = true;
decoder.receivePart(qrtext);
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) {
try {
PSBT psbt = new PSBT(urResult.ur.toBytes());
result = new Result(psbt);
return;
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
try {
Transaction transaction = new Transaction(urResult.ur.toBytes());
result = new Result(transaction);
return;
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction");
} else {
result = new Result("Cannot parse UR type of " + urResult.ur.getType());
}
} else {
result = new Result(urResult.error);
}
}
} else {
PSBT psbt;
Transaction transaction;
BitcoinURI bitcoinURI;
Address address;
try {
bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI);
return;
} catch(Exception e) {
//Ignore, not an BIP 21 URI
}
try {
address = Address.fromString(qrtext);
result = new Result(address);
return;
} catch(Exception e) {
//Ignore, not an address
}
try {
psbt = PSBT.fromString(qrtext);
result = new Result(psbt);
return;
} catch(Exception e) {
//Ignore, not parseable as Base64 or hex
}
try {
psbt = new PSBT(qrResult.getRawBytes());
result = new Result(psbt);
return;
} catch(Exception e) {
//Ignore, not parseable as raw bytes
}
try {
transaction = new Transaction(Utils.hexToBytes(qrtext));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as hex
}
try {
transaction = new Transaction(qrResult.getRawBytes());
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as raw bytes
}
//Try Base43 used by Electrum
try {
psbt = new PSBT(Base43.decode(qrResult.getText()));
result = new Result(psbt);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
try {
transaction = new Transaction(Base43.decode(qrResult.getText()));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
result = new Result("Cannot parse QR code into a PSBT, transaction or address");
}
}
}
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
public final BitcoinURI uri;
public final String error;
public final Throwable exception;
public Result(Transaction transaction) {
this.transaction = transaction;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = null;
}
public Result(PSBT psbt) {
this.transaction = null;
this.psbt = psbt;
this.uri = null;
this.error = null;
this.exception = null;
}
public Result(BitcoinURI uri) {
this.transaction = null;
this.psbt = null;
this.uri = uri;
this.error = null;
this.exception = null;
}
public Result(Address address) {
this.transaction = null;
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.error = null;
this.exception = null;
}
public Result(String error) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = error;
this.exception = null;
}
public Result(Throwable exception) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = exception;
}
}
}
@@ -0,0 +1,75 @@
package com.sparrowwallet.sparrow.control;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.TreeItem;
import javafx.util.Callback;
import java.util.List;
import java.util.stream.Collectors;
public class RecursiveTreeItem<T> extends TreeItem<T> {
private final Callback<T, ObservableList<T>> childrenFactory;
private final Callback<T, Node> graphicsFactory;
public RecursiveTreeItem(Callback<T, ObservableList<T>> childrenFactory){
this(null, childrenFactory);
}
public RecursiveTreeItem(final T value, Callback<T, ObservableList<T>> childrenFactory){
this(value, (item) -> null, childrenFactory);
}
public RecursiveTreeItem(final T value, Callback<T, Node> graphicsFactory, Callback<T, ObservableList<T>> childrenFactory){
super(value, graphicsFactory.call(value));
this.graphicsFactory = graphicsFactory;
this.childrenFactory = childrenFactory;
if(value != null) {
addChildrenListener(value);
}
valueProperty().addListener((obs, oldValue, newValue)->{
if(newValue != null){
addChildrenListener(newValue);
}
});
this.setExpanded(false);
}
private void addChildrenListener(T value){
final ObservableList<T> children = childrenFactory.call(value);
children.forEach(child -> RecursiveTreeItem.this.getChildren().add(
new RecursiveTreeItem<>(child, this.graphicsFactory, childrenFactory)));
children.addListener((ListChangeListener<T>) change -> {
while(change.next()){
if(change.wasAdded()){
if(change.getFrom() >= RecursiveTreeItem.this.getChildren().size()) {
change.getAddedSubList().forEach(t-> RecursiveTreeItem.this.getChildren().add(new RecursiveTreeItem<>(t, this.graphicsFactory, childrenFactory)));
} else {
change.getAddedSubList().forEach(t-> RecursiveTreeItem.this.getChildren().add(change.getFrom(), new RecursiveTreeItem<>(t, this.graphicsFactory, childrenFactory)));
}
}
if(change.wasRemoved()){
change.getRemoved().forEach(t->{
final List<TreeItem<T>> itemsToRemove = RecursiveTreeItem.this
.getChildren()
.stream()
.filter(treeItem -> treeItem.getValue().equals(t))
.collect(Collectors.toList());
RecursiveTreeItem.this.getChildren().removeAll(itemsToRemove);
});
}
}
});
}
}
@@ -0,0 +1,99 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu;
import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration;
import org.fxmisc.richtext.CodeArea;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
public class ScriptArea extends CodeArea {
private Script script;
public void appendScript(Script script) {
appendScript(script, null, null);
}
public void appendScript(Script script, Script redeemScript, Script witnessScript) {
if(this.script == null) {
this.script = script;
ScriptContextMenu contextMenu = new ScriptContextMenu(this, script);
setContextMenu(contextMenu);
}
if(P2PKH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append(script.getChunks().get(1).toString(), "script-opcode");
append(" ", "");
append("<pkh>", "script-hash");
append(" ", "");
append(script.getChunks().get(3).toString(), "script-opcode");
append(" ", "");
append(script.getChunks().get(4).toString(), "script-opcode");
} else if(P2SH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<sh>", "script-hash");
append(" ", "");
append(script.getChunks().get(2).toString(), "script-opcode");
} else if(P2WPKH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<wpkh>", "script-hash");
} else if(P2WSH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<wsh>", "script-hash");
} else {
int signatureCount = 1;
int pubKeyCount = 1;
for (int i = 0; i < script.getChunks().size(); i++) {
ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) {
append(chunk.toString(), "script-opcode");
} else if(chunk.isSignature()) {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isScript()) {
Script nestedScript = chunk.getScript();
if (nestedScript.equals(redeemScript)) {
append("<RedeemScript>", "script-redeem");
} else if(nestedScript.equals(witnessScript)) {
append("<WitnessScript>", "script-redeem");
} else {
append("(", "script-nest");
appendScript(nestedScript);
append(")", "script-nest");
}
} else {
append(chunk.toString(), "script-other");
}
if(i < script.getChunks().size() - 1) {
append(" ", "");
}
}
}
}
public void clear() {
super.clear();
this.script = null;
setDisable(false);
setContextMenu(null);
Decorator.removeAllDecorations(this);
}
public void addPSBTDecoration(String description, String styleClass) {
Decorator.addDecoration(this, new GraphicDecoration(new TextDecoration("PSBT", description, styleClass), Pos.TOP_RIGHT));
}
public Script getScript() {
return script;
}
}
@@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppController;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
public class SeedDisplayDialog extends Dialog<Void> {
public SeedDisplayDialog(Keystore decryptedKeystore) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 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(74 + 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);
MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore);
keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane);
stackPane.getChildren().addAll(anchorPane);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(150 + height);
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
}
}
@@ -0,0 +1,158 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreSignedEvent;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.control.SegmentedBar;
public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.SignatureProgressSegment> {
public SignaturesProgressBar() {
setOrientation(Orientation.HORIZONTAL);
setSegmentViewFactory(SignatureProgressSegmentView::new);
setInfoNodeFactory(segment -> segment.getKeystore() == null ? null : new SignatureProgressSegmentLabel(segment.getKeystore().getLabel()));
}
public void initialize(ObservableList<Keystore> signedKeystores, int threshold) {
getStyleClass().add("signatures-progress-bar");
getSegments().clear();
int numSegments = Math.max(threshold, signedKeystores.size());
double segmentSize = 100d / numSegments;
for(int i = 0; i < numSegments; i++) {
if(i < signedKeystores.size()) {
getSegments().add(new SignatureProgressSegment(segmentSize, i, signedKeystores.get(i)));
} else {
getSegments().add(new SignatureProgressSegment(segmentSize, i, null));
}
}
signedKeystores.addListener((ListChangeListener<Keystore>) c -> {
int numSegments1 = Math.max(threshold, c.getList().size());
double newSegmentSize = 100d / numSegments1;
for(int i = 0; i < numSegments1; i++) {
SignatureProgressSegment segment = null;
if(i < getSegments().size()) {
segment = getSegments().get(i);
}
Keystore signedKeystore = null;
if(i < signedKeystores.size()) {
signedKeystore = signedKeystores.get(i);
}
if(segment != null) {
//Animate new signature if changed
segment.setKeystore(signedKeystore);
} else {
//Add extra (unnecessary) signature
for(SignaturesProgressBar.SignatureProgressSegment existingSegment : getSegments()) {
existingSegment.setValue(newSegmentSize);
}
SignaturesProgressBar.SignatureProgressSegment newSegment = new SignatureProgressSegment(newSegmentSize, i, null);
getSegments().add(newSegment);
newSegment.setKeystore(signedKeystore);
}
}
});
}
public static class SignatureProgressSegment extends SegmentedBar.Segment {
private final SimpleObjectProperty<Keystore> keystoreProperty;
private final int index;
public SignatureProgressSegment(double value, int index, Keystore keystore) {
super(value);
this.index = index;
this.keystoreProperty = new SimpleObjectProperty<>(this, "keystore", null);
keystoreProperty.addListener((observable, oldValue, newValue) -> {
setText(newValue == null ? "No keystore" : newValue.getLabel());
});
setKeystore(keystore);
}
public int getIndex() {
return index;
}
public Keystore getKeystore() {
return keystoreProperty.get();
}
public SimpleObjectProperty<Keystore> keystoreProperty() {
return keystoreProperty;
}
public void setKeystore(Keystore keystore) {
keystoreProperty.set(keystore);
}
public void signatureCompleted() {
EventManager.get().post(new KeystoreSignedEvent(getKeystore()));
}
}
public static class SignatureProgressSegmentView extends StackPane {
private final ProgressBar progressBar;
private final Label label;
public SignatureProgressSegmentView(SignatureProgressSegment segment) {
getStyleClass().add("signature-progress-segment");
getStyleClass().add("segment" + segment.getIndex());
label = new Label();
label.textProperty().bind(segment.textProperty());
label.getStyleClass().add("signature-progress-segment-label");
StackPane.setAlignment(label, Pos.CENTER);
progressBar = new ProgressBar(segment.getKeystore() == null ? 0.0 : 1.0);
progressBar.setPrefWidth(Double.MAX_VALUE);
progressBar.setPrefHeight(30);
setPrefHeight(50);
getChildren().addAll(progressBar, label);
segment.keystoreProperty().addListener((observable, oldValue, newValue) -> {
if(oldValue == null && newValue != null) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(progressBar.progressProperty(), 0)),
new KeyFrame(Duration.millis(800), e -> {
segment.signatureCompleted();
}, new KeyValue(progressBar.progressProperty(), 1))
);
timeline.setCycleCount(1);
timeline.play();
}
});
}
@Override
protected void layoutChildren() {
super.layoutChildren();
label.setVisible(label.prefWidth(-1) < getWidth() - getPadding().getLeft() - getPadding().getRight() - 8);
}
}
public static class SignatureProgressSegmentLabel extends Label {
public SignatureProgressSegmentLabel(String text) {
super(text);
setPadding(new Insets(10));
}
}
}
@@ -0,0 +1,102 @@
package com.sparrowwallet.sparrow.control;
import java.text.DecimalFormatSymbols;
import java.util.regex.Pattern;
import javafx.beans.NamedArg;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
public class TextFieldValidator {
private static final String CURRENCY_SYMBOL = DecimalFormatSymbols.getInstance().getCurrencySymbol();
private static final String DECIMAL_SEPARATOR = ".";
private final Pattern INPUT_PATTERN;
public TextFieldValidator(@NamedArg("modus") ValidationModus modus, @NamedArg("countOf") int countOf) {
this(modus.createPattern(countOf));
}
public TextFieldValidator(@NamedArg("regex") String regex) {
this(Pattern.compile(regex));
}
public TextFieldValidator(Pattern inputPattern) {
INPUT_PATTERN = inputPattern;
}
public static TextFieldValidator maxFractionDigits(int countOf) {
return new TextFieldValidator(maxFractionPattern(countOf));
}
public static TextFieldValidator maxIntegers(int countOf) {
return new TextFieldValidator(maxIntegerPattern(countOf));
}
public static TextFieldValidator integersOnly() {
return new TextFieldValidator(integersOnlyPattern());
}
public TextFormatter<String> getFormatter() {
return new TextFormatter<>(TextFormatter.IDENTITY_STRING_CONVERTER, "", this::validateChange);
}
private Change validateChange(Change c) {
if (validate(c.getControlNewText())) {
return c;
}
return null;
}
public boolean validate(String input) {
return INPUT_PATTERN.matcher(input).matches();
}
private static Pattern maxFractionPattern(int countOf) {
return Pattern.compile("\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?");
}
private static Pattern maxCurrencyFractionPattern(int countOf) {
return Pattern.compile("^\\\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?\\s?\\\\" + CURRENCY_SYMBOL + "?");
}
private static Pattern maxIntegerPattern(int countOf) {
return Pattern.compile("\\d{0," + countOf + "}");
}
private static Pattern integersOnlyPattern() {
return Pattern.compile("\\d*");
}
public enum ValidationModus {
MAX_CURRENCY_FRACTION_DIGITS {
@Override
public Pattern createPattern(int countOf) {
return maxCurrencyFractionPattern(countOf);
}
},
MAX_FRACTION_DIGITS {
@Override
public Pattern createPattern(int countOf) {
return maxFractionPattern(countOf);
}
},
MAX_INTEGERS {
@Override
public Pattern createPattern(int countOf) {
return maxIntegerPattern(countOf);
}
},
INTEGERS_ONLY {
@Override
public Pattern createPattern(int countOf) {
return integersOnlyPattern();
}
};
public abstract Pattern createPattern(int countOf);
}
}
@@ -0,0 +1,157 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import java.util.Arrays;
import java.util.OptionalDouble;
public class TitledDescriptionPane extends TitledPane {
private Label descriptionLabel;
protected Hyperlink showHideLink;
protected HBox buttonBox;
public TitledDescriptionPane(String title, String description, String content, String imageUrl) {
getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
getStyleClass().add("titled-description-pane");
setPadding(Insets.EMPTY);
setGraphic(getTitle(title, description, imageUrl));
setContent(getContentBox(content));
removeArrow();
}
protected Node getTitle(String title, String description, String imageUrl) {
HBox listItem = new HBox();
listItem.setPadding(new Insets(10, 20, 10, 10));
listItem.setSpacing(10);
HBox imageBox = new HBox();
imageBox.setMinWidth(50);
imageBox.setMinHeight(50);
listItem.getChildren().add(imageBox);
Image image = new Image(imageUrl, 50, 50, true, true);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setImage(image);
imageBox.getChildren().add(imageView);
}
VBox labelsBox = new VBox();
labelsBox.setSpacing(5);
labelsBox.setAlignment(Pos.CENTER_LEFT);
Label mainLabel = new Label();
mainLabel.setText(title);
mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel);
HBox descriptionBox = new HBox();
descriptionBox.setSpacing(7);
labelsBox.getChildren().add(descriptionBox);
descriptionLabel = new Label(description);
descriptionLabel.getStyleClass().add("description-label");
showHideLink = new Hyperlink("Show Details...");
showHideLink.managedProperty().bind(showHideLink.visibleProperty());
showHideLink.setOnAction(event -> {
setExpanded(!this.isExpanded());
});
this.expandedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
showHideLink.setText(showHideLink.getText().replace("Show", "Hide"));
} else {
showHideLink.setText(showHideLink.getText().replace("Hide", "Show"));
}
});
descriptionBox.getChildren().addAll(descriptionLabel, showHideLink);
listItem.getChildren().add(labelsBox);
HBox.setHgrow(labelsBox, Priority.ALWAYS);
buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT);
Control button = createButton();
if(button != null) {
buttonBox.getChildren().add(button);
}
listItem.getChildren().add(buttonBox);
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
//Hack to force listItem to expand to full available width less border
listItem.setPrefWidth(newValue.getWidth() - 2);
});
return listItem;
}
protected Control createButton() {
//No buttons by default
return null;
}
protected void setDescription(String text) {
descriptionLabel.getStyleClass().remove("description-error");
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");
descriptionLabel.setText(title);
if(detail != null && !detail.isEmpty()) {
setContent(getContentBox(detail));
setExpanded(true);
}
}
protected Node getContentBox(String message) {
Label details = new Label(message);
details.setWrapText(true);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_LEFT);
contentBox.getChildren().add(details);
contentBox.setPadding(new Insets(10, 30, 10, 30));
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
double numLines = Math.max(1, Math.ceil(width / 400d));
//Handle long words like txids
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
if(maxWordLength.isPresent() && maxWordLength.getAsDouble() > 300.0) {
numLines += 1.0;
}
double height = Math.max(60, numLines * 20);
contentBox.setPrefHeight(height);
return contentBox;
}
private void removeArrow() {
removeArrow(0);
}
private void removeArrow(int count) {
Platform.runLater(() -> {
Node arrow = this.lookup(".arrow");
if (arrow != null) {
arrow.setVisible(false);
arrow.setManaged(false);
} else if(count < 20) {
removeArrow(count+1);
}
});
}
}
@@ -0,0 +1,378 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.util.*;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final double DIAGRAM_HEIGHT = 230.0;
private WalletTransaction walletTx;
public void update(WalletTransaction walletTx) {
if(walletTx == null) {
getChildren().clear();
} else {
this.walletTx = walletTx;
update();
}
}
public void update() {
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = getDisplayedUtxos();
Pane inputsTypePane = getInputsType(displayedUtxos);
GridPane.setConstraints(inputsTypePane, 0, 0);
Pane inputsPane = getInputsLabels(displayedUtxos);
GridPane.setConstraints(inputsPane, 1, 0);
Node inputsLinesPane = getInputsLines(displayedUtxos);
GridPane.setConstraints(inputsLinesPane, 2, 0);
Pane txPane = getTransactionPane();
GridPane.setConstraints(txPane, 3, 0);
Pane outputsLinesPane = getOutputsLines();
GridPane.setConstraints(outputsLinesPane, 4, 0);
Pane outputsPane = getOutputsLabels();
GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear();
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
}
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
if(selectedUtxos.size() > MAX_UTXOS) {
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
List<BlockTransactionHashIndex> additional = new ArrayList<>();
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
if (utxos.size() < MAX_UTXOS) {
utxos.put(reference, selectedUtxos.get(reference));
} else {
additional.add(reference);
}
}
utxos.put(new AdditionalBlockTransactionHashIndex(additional), null);
return utxos;
} else {
return selectedUtxos;
}
}
private Pane getInputsType(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
StackPane stackPane = new StackPane();
if(walletTx.isCoinControlUsed()) {
VBox pane = new VBox();
double width = 22.0;
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
Line widthLine = new Line();
widthLine.setStartX(0);
widthLine.setEndX(width);
widthLine.getStyleClass().add("boundary");
Line topYaxis = new Line();
topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(DIAGRAM_HEIGHT * 0.5 - 20.0);
topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(0);
topYaxis.getStyleClass().add("inputs-type");
Line topBracket = new Line();
topBracket.setStartX(width * 0.5);
topBracket.setStartY(0);
topBracket.setEndX(width);
topBracket.setEndY(0);
topBracket.getStyleClass().add("inputs-type");
Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(DIAGRAM_HEIGHT);
bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(DIAGRAM_HEIGHT * 0.5 + 20.0);
bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(DIAGRAM_HEIGHT);
bottomBracket.setEndX(width);
bottomBracket.setEndY(DIAGRAM_HEIGHT);
bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
pane.getChildren().add(group);
Glyph lockGlyph = getLockGlyph();
lockGlyph.getStyleClass().add("inputs-type");
Tooltip tooltip = new Tooltip("Coin control active");
lockGlyph.setTooltip(tooltip);
stackPane.getChildren().addAll(pane, lockGlyph);
}
return stackPane;
}
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
VBox inputsBox = new VBox();
inputsBox.setMaxWidth(150);
inputsBox.setPadding(new Insets(0, 10, 0, 10));
inputsBox.minHeightProperty().bind(minHeightProperty());
inputsBox.setAlignment(Pos.CENTER_RIGHT);
inputsBox.getChildren().add(createSpacer());
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
WalletNode walletNode = displayedUtxos.get(input);
String desc = getInputDescription(input);
Label label = new Label(desc);
Tooltip tooltip = new Tooltip();
if(walletNode != null) {
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
if(input.getLabel() == null || input.getLabel().isEmpty()) {
label.getStyleClass().add("input-label");
} else {
tooltip.getStyleClass().add("input-label");
}
} else {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex)input;
StringJoiner joiner = new StringJoiner("\n");
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(getInputDescription(additionalInput));
}
tooltip.setText(joiner.toString());
tooltip.getStyleClass().add("input-label");
}
label.setTooltip(tooltip);
inputsBox.getChildren().add(label);
inputsBox.getChildren().add(createSpacer());
}
return inputsBox;
}
private String getInputDescription(BlockTransactionHashIndex input) {
return input.getLabel() != null && !input.getLabel().isEmpty() ? input.getLabel() : input.getHashAsString().substring(0, 8) + "..:" + input.getIndex();
}
private String getSatsValue(long amount) {
return String.format(Locale.ENGLISH, "%,d", amount);
}
private Pane getInputsLines(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
VBox pane = new VBox();
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
Line yaxisLine = new Line();
yaxisLine.setStartX(0);
yaxisLine.setStartY(0);
yaxisLine.setEndX(0);
yaxisLine.setEndY(DIAGRAM_HEIGHT);
yaxisLine.getStyleClass().add("boundary");
group.getChildren().add(yaxisLine);
double width = 140.0;
int numUtxos = displayedUtxos.size();
for(int i = 1; i <= numUtxos; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("input-line");
curve.setStartX(0);
double scaleFactor = (double)i / (numUtxos + 1);
int nodeHeight = 17;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setStartY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setEndX(width);
curve.setEndY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setControlX1(scale(width, 0.2, 0));
curve.setControlY1(curve.getStartY());
curve.setControlX2(scale(width, 0.8, 0));
curve.setControlY2(curve.getEndY());
group.getChildren().add(curve);
}
pane.getChildren().add(group);
return pane;
}
private static double scale(Double value, double scaleFactor, double additional) {
return value * (1.0 - scaleFactor) + additional;
}
private Pane getOutputsLines() {
VBox pane = new VBox();
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
Line yaxisLine = new Line();
yaxisLine.setStartX(0);
yaxisLine.setStartY(0);
yaxisLine.setEndX(0);
yaxisLine.endYProperty().bind(this.heightProperty());
yaxisLine.getStyleClass().add("boundary");
group.getChildren().add(yaxisLine);
double width = 140.0;
int numOutputs = (walletTx.getChangeNode() == null ? 2 : 3);
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
curve.setStartX(0);
curve.setStartY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setEndX(width);
double scaleFactor = (double)i / (numOutputs + 1);
int nodeHeight = 20;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setEndY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setControlX1(scale(width, 0.2, 0));
curve.controlY1Property().bind(curve.startYProperty());
curve.setControlX2(scale(width, 0.8, 0));
curve.controlY2Property().bind(curve.endYProperty());
group.getChildren().add(curve);
}
pane.getChildren().add(group);
return pane;
}
private Pane getOutputsLabels() {
VBox outputsBox = new VBox();
outputsBox.setMaxWidth(150);
outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer());
String recipientDesc = walletTx.getRecipientAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, getSendGlyph());
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
Tooltip recipientTooltip = new Tooltip("Send " + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
outputsBox.getChildren().add(createSpacer());
if(walletTx.getChangeNode() != null) {
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
changeLabel.setTooltip(changeTooltip);
outputsBox.getChildren().add(changeLabel);
outputsBox.getChildren().add(createSpacer());
}
boolean highFee = (walletTx.getFeePercentage() > 0.1);
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
feeTooltip.getStyleClass().add("fee-tooltip");
feeLabel.setTooltip(feeTooltip);
outputsBox.getChildren().add(feeLabel);
outputsBox.getChildren().add(createSpacer());
return outputsBox;
}
private Pane getTransactionPane() {
VBox txPane = new VBox();
txPane.setPadding(new Insets(0, 10, 0, 10));
txPane.setAlignment(Pos.CENTER);
txPane.getChildren().add(createSpacer());
String txDesc = "Transaction";
Label txLabel = new Label(txDesc);
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
txLabel.setTooltip(tooltip);
txPane.getChildren().add(txLabel);
txPane.getChildren().add(createSpacer());
return txPane;
}
private Node createSpacer() {
final Region spacer = new Region();
VBox.setVgrow(spacer, Priority.ALWAYS);
return spacer;
}
private Glyph getSendGlyph() {
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.getStyleClass().add("send-icon");
sendGlyph.setFontSize(12);
return sendGlyph;
}
private Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
return changeGlyph;
}
private Glyph getFeeGlyph() {
Glyph feeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.HAND_HOLDING);
feeGlyph.getStyleClass().add("fee-icon");
feeGlyph.setFontSize(12);
return feeGlyph;
}
private Glyph getWarningGlyph() {
Glyph feeWarningGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12);
return feeWarningGlyph;
}
private Glyph getLockGlyph() {
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.LOCK);
lockGlyph.getStyleClass().add("lock-icon");
lockGlyph.setFontSize(12);
return lockGlyph;
}
private static class AdditionalBlockTransactionHashIndex extends BlockTransactionHashIndex {
private final List<BlockTransactionHashIndex> additionalInputs;
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
this.additionalInputs = additionalInputs;
}
@Override
public String getLabel() {
return additionalInputs.size() + " more...";
}
public List<BlockTransactionHashIndex> getAdditionalInputs() {
return additionalInputs;
}
}
}
@@ -0,0 +1,172 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
import javafx.application.Platform;
import org.fxmisc.richtext.CodeArea;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class TransactionHexArea extends CodeArea {
private static final int TRUNCATE_AT = 30000;
private static final int SEGMENTS_INTERVAL = 250;
private List<TransactionSegment> previousSegmentList = new ArrayList<>();
public void setTransaction(Transaction transaction) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
transaction.bitcoinSerializeToStream(baos);
String hex = Utils.bytesToHex(baos.toByteArray());
if(hex.length() > TRUNCATE_AT) {
hex = hex.substring(0, TRUNCATE_AT);
hex += "[truncated]";
}
clear();
appendText(hex);
previousSegmentList = new ArrayList<>();
} catch (IOException e) {
throw new IllegalStateException("Can't happen");
}
}
public void applyHighlighting(Transaction transaction, int selectedInputIndex, int selectedOutputIndex) {
List<TransactionSegment> segments = getTransactionSegments(transaction, selectedInputIndex, selectedOutputIndex);
List<TransactionSegment> changedSegments = new ArrayList<>(segments);
changedSegments.removeAll(previousSegmentList);
applyHighlighting(0, changedSegments);
previousSegmentList = segments;
}
private void applyHighlighting(int start, List<TransactionSegment> segments) {
int end = Math.min(segments.size(), start + SEGMENTS_INTERVAL);
for(int i = start; i < end; i++) {
TransactionSegment segment = segments.get(i);
if(segment.start < TRUNCATE_AT) {
setStyleClass(segment.start, Math.min(TRUNCATE_AT, segment.start + segment.length), segment.style);
}
}
if(end < segments.size()) {
Platform.runLater(() -> {
applyHighlighting(end, segments);
});
}
}
public List<TransactionSegment> getTransactionSegments(Transaction transaction, int selectedInputIndex, int selectedOutputIndex) {
List<TransactionSegment> segments = new ArrayList<>();
int cursor = 0;
//Version
cursor = addSegment(segments, cursor, 8, "version");
if(transaction.isSegwit()) {
//Segwit marker
cursor = addSegment(segments, cursor, 2, "segwit-marker");
//Segwit flag
cursor = addSegment(segments, cursor, 2, "segwit-flag");
}
//Number of inputs
VarInt numInputs = new VarInt(transaction.getInputs().size());
cursor = addSegment(segments, cursor, numInputs.getSizeInBytes() * 2, "num-inputs");
//Inputs
for(int i = 0; i < transaction.getInputs().size(); i++) {
TransactionInput input = transaction.getInputs().get(i);
cursor = addSegment(segments, cursor, 32 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "hash"));
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "index"));
VarInt scriptLen = new VarInt(input.getScriptBytes().length);
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sigscript"));
cursor = addSegment(segments, cursor, 4 * 2, "input-" + getIndexedStyleClass(i, selectedInputIndex, "sequence"));
}
//Number of outputs
VarInt numOutputs = new VarInt(transaction.getOutputs().size());
cursor = addSegment(segments, cursor, numOutputs.getSizeInBytes() * 2, "num-outputs");
//Outputs
for(int i = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput output = transaction.getOutputs().get(i);
cursor = addSegment(segments, cursor, 8 * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "value"));
VarInt scriptLen = new VarInt(output.getScriptBytes().length);
cursor = addSegment(segments, cursor, scriptLen.getSizeInBytes() * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript-length"));
cursor = addSegment(segments, cursor, (int) scriptLen.value * 2, "output-" + getIndexedStyleClass(i, selectedOutputIndex, "pubkeyscript"));
}
if(transaction.hasWitnesses()) {
for (int i = 0; i < transaction.getInputs().size(); i++) {
TransactionInput input = transaction.getInputs().get(i);
if (input.hasWitness()) {
TransactionWitness witness = input.getWitness();
VarInt witnessCount = new VarInt(witness.getPushCount());
cursor = addSegment(segments, cursor, witnessCount.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "count"));
for (byte[] push : witness.getPushes()) {
VarInt witnessLen = new VarInt(push.length);
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
}
}
}
}
//Locktime
cursor = addSegment(segments, cursor, 8, "locktime");
if(cursor != getLength()) {
//While this is normally a good sanity check, the truncation applied means it may fail, so it is left commented out
//throw new IllegalStateException("Cursor position does not match transaction serialisation " + cursor + ": " + getLength());
}
return segments;
}
private int addSegment(List<TransactionSegment> segments, int start, int length, String style) {
segments.add(new TransactionSegment(start, length, style));
return start + length;
}
private String getIndexedStyleClass(int iterableIndex, int selectedIndex, String styleClass) {
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
return styleClass;
}
return "other";
}
private static class TransactionSegment {
public TransactionSegment(int start, int length, String style) {
this.start = start;
this.length = length;
this.style = style;
}
public int start;
public int length;
public String style;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TransactionSegment segment = (TransactionSegment) o;
return start == segment.start &&
length == segment.length &&
style.equals(segment.style);
}
@Override
public int hashCode() {
return Objects.hash(start, length, style);
}
}
}
@@ -0,0 +1,66 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.AppController;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class TransactionIdDialog extends Dialog<Sha256Hash> {
private final CustomTextField txid;
public TransactionIdDialog() {
this.txid = (CustomTextField) TextFields.createClearableTextField();
txid.setFont(Font.font ("Courier", txid.getFont().getSize()));
final DialogPane dialogPane = getDialogPane();
setTitle("Load Transaction");
dialogPane.setHeaderText("Enter the transaction ID:");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(550);
dialogPane.setPrefHeight(200);
Glyph wallet = new Glyph("FontAwesome", FontAwesome.Glyph.BITCOIN);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
final VBox content = new VBox(10);
content.getChildren().add(txid);
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.registerValidator(txid, Validator.combine(
Validator.createEmptyValidator("Transaction id is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID length incorrect", newValue.length() != 64),
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID must be hexadecimal", !Utils.isHex(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Open Transaction", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
txid.getText().length() != 64 || !Utils.isHex(txid.getText()), txid.textProperty());
okButton.disableProperty().bind(isInvalid);
txid.setPromptText("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16");
Platform.runLater(txid::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? Sha256Hash.wrap(txid.getText()) : null);
}
}
@@ -0,0 +1,84 @@
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;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
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());
updateAll(rootEntry);
setShowRoot(false);
TreeTableColumn<Entry, Entry> dateCol = new TreeTableColumn<>("Date");
dateCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
dateCol.setCellFactory(p -> new EntryCell());
dateCol.setSortable(true);
getColumns().add(dateCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new LabelCell());
labelCol.setSortable(true);
getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new CoinCell());
amountCol.setSortable(true);
getColumns().add(amountCol);
TreeTableColumn<Entry, Number> balanceCol = new TreeTableColumn<>("Balance");
balanceCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return param.getValue().getValue() instanceof TransactionEntry ? ((TransactionEntry)param.getValue().getValue()).balanceProperty() : new ReadOnlyObjectWrapper<>(null);
});
balanceCol.setCellFactory(p -> new CoinCell());
balanceCol.setSortable(true);
getColumns().add(balanceCol);
setPlaceholder(new Label("No transactions"));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
getSortOrder().add(dateCol);
}
public void updateAll(WalletTransactionsEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
if(getColumns().size() > 0 && getSortOrder().isEmpty()) {
TreeTableColumn<Entry, ?> dateCol = getColumns().get(0);
getSortOrder().add(dateCol);
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
sort();
}
public void updateLabel(Entry entry) {
Entry rootEntry = getRoot().getValue();
rootEntry.updateLabel(entry);
}
}
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow.control;
import impl.org.controlsfx.skin.ToggleSwitchSkin;
import javafx.scene.control.Skin;
import org.controlsfx.control.ToggleSwitch;
public class UnlabeledToggleSwitch extends ToggleSwitch {
@Override protected Skin<?> createDefaultSkin() {
return new ToggleSwitchSkin(this) {
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset) - 20;
}
};
}
}
@@ -0,0 +1,51 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.*;
import org.controlsfx.glyphfont.Glyph;
import java.util.List;
public class UsbStatusButton extends MenuButton {
public UsbStatusButton() {
super("");
setGraphic(getIcon());
getStyleClass().add("status-bar-button");
this.setPopupSide(Side.RIGHT);
}
public void setDevices(List<Device> devices) {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(device.getNeedsPinSent()) {
deviceItem.setGraphic(getLockIcon());
} else {
deviceItem.setGraphic(getLockOpenIcon());
}
getItems().add(deviceItem);
}
}
private Node getIcon() {
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
usb.setFontSize(15);
return usb;
}
private Node getLockIcon() {
Glyph usb = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
usb.setFontSize(12);
return usb;
}
private Node getLockOpenIcon() {
Glyph usb = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK_OPEN);
usb.setFontSize(12);
return usb;
}
}
@@ -0,0 +1,110 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class UtxosChart extends BarChart<String, Number> {
private static final int MAX_BARS = 8;
private static final String OTHER_CATEGORY = "Other";
private List<Entry> selectedEntries;
private int totalUtxos;
private XYChart.Series<String, Number> utxoSeries;
public UtxosChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize(WalletUtxosEntry walletUtxosEntry) {
utxoSeries = new XYChart.Series<>();
getData().add(utxoSeries);
update(walletUtxosEntry);
BitcoinUnit unit = Config.get().getBitcoinUnit();
setBitcoinUnit(walletUtxosEntry.getWallet(), unit);
}
public void update(WalletUtxosEntry walletUtxosEntry) {
List<Data<String, Number>> utxoDataList = walletUtxosEntry.getChildren().stream()
.map(entry -> new XYChart.Data<>(getCategoryName(entry), (Number)entry.getValue(), entry))
.sorted((o1, o2) -> (int) (o2.getYValue().longValue() - o1.getYValue().longValue()))
.collect(Collectors.toList());
totalUtxos = utxoDataList.size();
if(utxoDataList.size() > MAX_BARS) {
Long otherTotal = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size()).stream().mapToLong(data -> data.getYValue().longValue()).sum();
utxoDataList = utxoDataList.subList(0, MAX_BARS - 1);
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal));
}
for(int i = 0; i < utxoDataList.size(); i++) {
XYChart.Data<String, Number> newData = utxoDataList.get(i);
if(i < utxoSeries.getData().size()) {
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() != null && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
utxoSeries.getData().set(i, newData);
}
} else {
utxoSeries.getData().add(newData);
}
}
if(utxoSeries.getData().size() > utxoDataList.size()) {
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
}
if(selectedEntries != null) {
select(selectedEntries);
}
}
private String getCategoryName(Entry entry) {
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." : entry.getLabel();
}
return ((UtxoEntry)entry).getDescription();
}
public void select(List<Entry> entries) {
Set<Node> selectedBars = lookupAll(".chart-bar.selected");
for(Node selectedBar : selectedBars) {
selectedBar.getStyleClass().remove("selected");
}
for(int i = 0; i < utxoSeries.getData().size(); i++) {
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
Node bar = lookup(".data" + i);
if(bar != null) {
if(data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) {
bar.getStyleClass().add("selected");
} else if(data.getExtraValue() == null && entries.size() == totalUtxos) {
bar.getStyleClass().add("selected");
}
}
}
this.selectedEntries = entries;
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
}
}
@@ -0,0 +1,104 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.List;
public class UtxosTreeTable extends CoinTreeTable {
public void initialize(WalletUtxosEntry rootEntry) {
getStyleClass().add("utxos-treetable");
setBitcoinUnit(rootEntry.getWallet());
updateAll(rootEntry);
setShowRoot(false);
TreeTableColumn<Entry, Entry> dateCol = new TreeTableColumn<>("Date");
dateCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
dateCol.setCellFactory(p -> new DateCell());
dateCol.setSortable(true);
getColumns().add(dateCol);
TreeTableColumn<Entry, Entry> outputCol = new TreeTableColumn<>("Output");
outputCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
outputCol.setCellFactory(p -> new EntryCell());
outputCol.setSortable(true);
outputCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getDescription().compareTo(entry2.getDescription());
});
getColumns().add(outputCol);
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new AddressCell());
addressCol.setSortable(true);
addressCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
});
getColumns().add(addressCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new LabelCell());
labelCol.setSortable(true);
getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new CoinCell());
amountCol.setSortable(true);
getColumns().add(amountCol);
setTreeColumn(amountCol);
setPlaceholder(new Label("No unspent outputs"));
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);
getSortOrder().add(amountCol);
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
public void updateAll(WalletUtxosEntry rootEntry) {
setBitcoinUnit(rootEntry.getWallet());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
setRoot(rootItem);
rootItem.setExpanded(true);
if(getColumns().size() > 0 && getSortOrder().isEmpty()) {
TreeTableColumn<Entry, ?> amountCol = getColumns().get(getColumns().size() - 1);
getSortOrder().add(amountCol);
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);
}
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Utxo entries should have already been updated, so only a resort required
sort();
}
public void updateLabel(Entry entry) {
Entry rootEntry = getRoot().getValue();
rootEntry.updateLabel(entry);
}
}
@@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ColdcardMultisig;
import com.sparrowwallet.sparrow.io.Electrum;
import com.sparrowwallet.sparrow.io.WalletExport;
import com.sparrowwallet.sparrow.io.WalletImport;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> {
private Wallet wallet;
public WalletExportDialog(Wallet wallet) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new Electrum());
} 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);
}
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(360);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@Subscribe
public void walletExported(WalletExportEvent event) {
wallet = event.getWallet();
setResult(wallet);
}
}
@@ -0,0 +1,60 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.List;
public class WalletImportDialog extends Dialog<Wallet> {
private Wallet wallet;
public WalletImportDialog() {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletImport> importers = List.of(new ColdcardMultisig(), new Electrum());
Accordion importAccordion = new Accordion();
for(WalletImport importer : importers) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
scrollPane.setContent(importAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@Subscribe
public void walletImported(WalletImportEvent event) {
wallet = event.getWallet();
setResult(wallet);
}
}
@@ -0,0 +1,62 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class WalletNameDialog extends Dialog<String> {
private final CustomTextField name;
public WalletNameDialog() {
this.name = (CustomTextField)TextFields.createClearableTextField();
final DialogPane dialogPane = getDialogPane();
setTitle("Wallet Name");
dialogPane.setHeaderText("Enter a name for this wallet:");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
final VBox content = new VBox(10);
content.getChildren().add(name);
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.registerValidator(name, Validator.combine(
Validator.createEmptyValidator("Wallet name is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("New Wallet", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
name.getText().length() == 0 || Storage.walletExists(name.getText()), name.textProperty());
okButton.disableProperty().bind(isInvalid);
name.setPromptText("Wallet Name");
Platform.runLater(name::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? name.getText() : null);
}
}
@@ -0,0 +1,116 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.sparrow.AppController;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
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;
public class WalletPasswordDialog extends Dialog<SecureString> {
private final ButtonType okButtonType;
private final PasswordRequirement requirement;
private final CustomPasswordField password;
private final CustomPasswordField passwordConfirm;
private final CheckBox backupExisting;
public WalletPasswordDialog(PasswordRequirement requirement) {
this(null, requirement);
}
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
this.requirement = requirement;
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
final DialogPane dialogPane = getDialogPane();
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText(walletName != null ? requirement.description.substring(0, requirement.description.length() - 1) + " for " + walletName + ":" : requirement.description);
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(260);
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lock.setFontSize(50);
dialogPane.setGraphic(lock);
final VBox content = new VBox(10);
content.setPrefHeight(100);
content.getChildren().add(password);
content.getChildren().add(passwordConfirm);
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(backupExisting);
backupExisting.setSelected(true);
}
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater( () -> {
validationSupport.registerValidator(passwordConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Password confirmation does not match", !passwordConfirm.getText().equals(password.getText())));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
okButtonType = new javafx.scene.control.ButtonType(requirement.okButtonText, ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
okButton.setPrefWidth(130);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
okButton.disableProperty().bind(isInvalid);
if(requirement != PasswordRequirement.UPDATE_NEW) {
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
}
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY) {
password.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue.isEmpty()) {
okButton.setText("No Password");
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
} else {
okButton.setText("Set Password");
passwordConfirm.setVisible(true);
passwordConfirm.setManaged(true);
}
});
}
password.setPromptText("Password");
Platform.runLater(password::requestFocus);
passwordConfirm.setPromptText("Password Confirmation");
setResultConverter(dialogButton -> dialogButton == okButtonType ? new SecureString(password.getText()) : null);
}
public boolean isBackupExisting() {
return backupExisting.isSelected();
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password");
private final String description;
private final String okButtonText;
PasswordRequirement(String description, String okButtonText) {
this.description = description;
this.okButtonText = okButtonText;
}
}
}
@@ -0,0 +1,81 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamResolution;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import java.awt.image.BufferedImage;
import java.util.concurrent.TimeUnit;
public class WebcamService extends Service<Image> {
private final WebcamResolution resolution ;
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
public WebcamService(WebcamResolution resolution) {
this.resolution = resolution;
}
@Override
public Task<Image> createTask() {
return new Task<Image>() {
@Override
protected Image call() throws Exception {
Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0);
try {
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
cam.open();
while(!isCancelled()) {
if(cam.isImageNew()) {
BufferedImage bimg = cam.getImage();
updateValue(SwingFXUtils.toFXImage(bimg, null));
readQR(bimg);
}
}
cam.close();
return getValue();
} finally {
cam.close();
}
}
};
}
private void readQR(BufferedImage bufferedImage) {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
resultProperty.set(result);
} catch(NotFoundException e) {
// fall thru, it means there is no QR code in image
}
}
public Result getResult() {
return resultProperty.get();
}
public ObjectProperty<Result> resultProperty() {
return resultProperty;
}
public int getCamWidth() {
return resolution.getSize().width;
}
public int getCamHeight() {
return resolution.getSize().height;
}
}
@@ -0,0 +1,100 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebcamView {
private static final Logger log = LoggerFactory.getLogger(WebcamView.class);
private final ImageView imageView;
private final WebcamService service;
private final Region view;
private final Label statusPlaceholder ;
public WebcamView(WebcamService service) {
this.service = service ;
this.imageView = new ImageView();
imageView.setPreserveRatio(true);
// make the cam behave like a mirror:
imageView.setScaleX(-1);
this.statusPlaceholder = new Label();
this.view = new Region() {
{
service.stateProperty().addListener((obs, oldState, newState) -> {
switch (newState) {
case READY:
statusPlaceholder.setText("Initializing");
getChildren().setAll(statusPlaceholder);
break ;
case SCHEDULED:
statusPlaceholder.setText("Waiting");
getChildren().setAll(statusPlaceholder);
break ;
case RUNNING:
imageView.imageProperty().unbind();
imageView.imageProperty().bind(service.valueProperty());
getChildren().setAll(imageView);
break ;
case CANCELLED:
imageView.imageProperty().unbind();
imageView.setImage(null);
statusPlaceholder.setText("Stopped");
getChildren().setAll(statusPlaceholder);
break;
case FAILED:
imageView.imageProperty().unbind();
statusPlaceholder.setText("Error");
getChildren().setAll(statusPlaceholder);
log.error("Failed to start web cam", service.getException());
break;
case SUCCEEDED:
// unreachable...
imageView.imageProperty().unbind();
statusPlaceholder.setText("");
getChildren().clear();
}
requestLayout();
});
}
@Override
protected void layoutChildren() {
super.layoutChildren();
double w = getWidth();
double h = getHeight();
if (service.isRunning()) {
imageView.setFitWidth(w);
imageView.setFitHeight(h);
imageView.resizeRelocate(0, 0, w, h);
} else {
double labelHeight = statusPlaceholder.prefHeight(w);
double labelWidth = statusPlaceholder.prefWidth(labelHeight);
statusPlaceholder.resizeRelocate((w - labelWidth)/2, (h-labelHeight)/2, labelWidth, labelHeight);
}
}
@Override
protected double computePrefWidth(double height) {
return service.getCamWidth();
}
@Override
protected double computePrefHeight(double width) {
return service.getCamHeight();
}
};
}
public WebcamService getService() {
return service;
}
public Node getView() {
return view;
}
}
@@ -0,0 +1,102 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.Mode;
import javafx.application.HostServices;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.HyperlinkLabel;
import org.controlsfx.control.StatusBar;
import org.controlsfx.control.ToggleSwitch;
public class WelcomeDialog extends Dialog<Mode> {
private static final String[] ELECTRUM_SERVERS = new String[]{
"ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx",
"electrs", "https://github.com/romanz/electrs",
"esplora-electrs", "https://github.com/Blockstream/electrs"};
private final HostServices hostServices;
public WelcomeDialog(HostServices services) {
this.hostServices = services;
final DialogPane dialogPane = getDialogPane();
setTitle("Welcome to Sparrow");
dialogPane.setHeaderText("Welcome to Sparrow!");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(480);
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.OK_DONE);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType("Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(onlineButtonType, offlineButtonType);
final VBox content = new VBox(20);
content.setPadding(new Insets(20, 20, 20, 20));
content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator."));
content.getChildren().add(createParagraph("For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy. Examples include:"));
VBox linkBox = new VBox();
for(int i = 0; i < ELECTRUM_SERVERS.length; i+=2) {
linkBox.getChildren().add(createBulletedLink(ELECTRUM_SERVERS[i], ELECTRUM_SERVERS[i+1]));
}
content.getChildren().add(linkBox);
content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar:"));
content.getChildren().add(createStatusBar(onlineButtonType, offlineButtonType));
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
}
private Label createParagraph(String text) {
Label label = new Label(text);
label.setWrapText(true);
return label;
}
private HyperlinkLabel createBulletedLink(String name, String url) {
HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + name + "]");
label.setOnAction(event -> {
hostServices.showDocument(url);
});
return label;
}
private StatusBar createStatusBar(ButtonType onlineButtonType, ButtonType offlineButtonType) {
StatusBar statusBar = new StatusBar();
statusBar.setText("Online Mode");
statusBar.getRightItems().add(createToggle(statusBar, onlineButtonType, offlineButtonType));
return statusBar;
}
private ToggleSwitch createToggle(StatusBar statusBar, ButtonType onlineButtonType, ButtonType offlineButtonType) {
ToggleSwitch toggleSwitch = new UnlabeledToggleSwitch();
toggleSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> {
Button onlineButton = (Button) getDialogPane().lookupButton(onlineButtonType);
onlineButton.setDefaultButton(newValue);
Button offlineButton = (Button) getDialogPane().lookupButton(offlineButtonType);
offlineButton.setDefaultButton(!newValue);
statusBar.setText(newValue ? "Online Mode" : "Offline Mode");
});
toggleSwitch.setSelected(true);
return toggleSwitch;
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
/**
* This event is used by the DeviceAddressDialog to indicate that a USB device has displayed an address
*
*/
public class AddressDisplayedEvent {
private final String address;
public AddressDisplayedEvent(String address) {
this.address = address;
}
public String getAddress() {
return address;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.BitcoinUnit;
public class BitcoinUnitChangedEvent {
private final BitcoinUnit bitcoinUnit;
public BitcoinUnitChangedEvent(BitcoinUnit bitcoinUnit) {
this.bitcoinUnit = bitcoinUnit;
}
public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit;
}
}
@@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import java.util.Map;
public class BlockTransactionFetchedEvent extends PagedEvent {
private final Sha256Hash txId;
private final BlockTransaction blockTransaction;
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
this(txId, blockTransaction, inputTransactions, 0, blockTransaction.getTransaction().getInputs().size());
}
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions, int pageStart, int pageEnd) {
super(pageStart, pageEnd);
this.txId = txId;
this.blockTransaction = blockTransaction;
this.inputTransactions = inputTransactions;
}
public Sha256Hash getTxId() {
return txId;
}
public BlockTransaction getBlockTransaction() {
return blockTransaction;
}
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
return inputTransactions;
}
}
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import java.util.List;
public class BlockTransactionOutputsFetchedEvent extends PagedEvent {
private final Sha256Hash txId;
private final List<BlockTransaction> outputTransactions;
public BlockTransactionOutputsFetchedEvent(Sha256Hash txId, List<BlockTransaction> outputTransactions, int pageStart, int pageEnd) {
super(pageStart, pageEnd);
this.txId = txId;
this.outputTransactions = outputTransactions;
}
public Sha256Hash getTxId() {
return txId;
}
public List<BlockTransaction> getOutputTransactions() {
return outputTransactions;
}
}
@@ -0,0 +1,37 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import java.util.List;
import java.util.Map;
public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final List<String> serverVersion;
private final String serverBanner;
private final int blockHeight;
private final BlockHeader blockHeader;
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates) {
super(targetBlockFeeRates);
this.serverVersion = serverVersion;
this.serverBanner = serverBanner;
this.blockHeight = blockHeight;
this.blockHeader = blockHeader;
}
public List<String> getServerVersion() {
return serverVersion;
}
public String getServerBanner() {
return serverBanner;
}
public int getBlockHeight() {
return blockHeight;
}
public BlockHeader getBlockHeader() {
return blockHeader;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class ConnectionFailedEvent {
private final Throwable exception;
public ConnectionFailedEvent(Throwable exception) {
this.exception = exception;
}
public Throwable getException() {
return exception;
}
}
@@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.CurrencyRate;
import java.util.Currency;
public class ExchangeRatesUpdatedEvent {
private final Currency currency;
private final Double btcRate;
public ExchangeRatesUpdatedEvent(Currency currency, Double btcRate) {
this.currency = currency;
this.btcRate = btcRate;
}
public Currency getCurrency() {
return currency;
}
public Double getBtcRate() {
return btcRate;
}
public CurrencyRate getCurrencyRate() {
return new CurrencyRate(currency, btcRate);
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import java.util.Map;
public class FeeRatesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates;
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates) {
this.targetBlockFeeRates = targetBlockFeeRates;
}
public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
}
@@ -0,0 +1,23 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.io.ExchangeSource;
import java.util.Currency;
public class FiatCurrencySelectedEvent {
private final ExchangeSource exchangeSource;
private final Currency currency;
public FiatCurrencySelectedEvent(ExchangeSource exchangeSource, Currency currency) {
this.exchangeSource = exchangeSource;
this.currency = currency;
}
public ExchangeSource getExchangeSource() {
return exchangeSource;
}
public Currency getCurrency() {
return currency;
}
}
@@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Wallet;
public class FinalizeTransactionEvent {
private final PSBT psbt;
private final Wallet signingWallet;
public FinalizeTransactionEvent(PSBT psbt, Wallet signingWallet) {
this.psbt = psbt;
this.signingWallet = signingWallet;
}
public PSBT getPsbt() {
return psbt;
}
public Wallet getSigningWallet() {
return signingWallet;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
public class KeystoreImportEvent {
private Keystore keystore;
public KeystoreImportEvent(Keystore keystore) {
this.keystore = keystore;
}
public Keystore getKeystore() {
return keystore;
}
}
@@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
/**
* This event is used to indicate the animation for signing a keystore is complete
*/
public class KeystoreSignedEvent {
private final Keystore keystore;
public KeystoreSignedEvent(Keystore keystore) {
this.keystore = keystore;
}
public Keystore getKeystore() {
return keystore;
}
}
@@ -0,0 +1,21 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader;
public class NewBlockEvent {
private final int height;
private final BlockHeader blockHeader;
public NewBlockEvent(int height, BlockHeader blockHeader) {
this.height = height;
this.blockHeader = blockHeader;
}
public int getHeight() {
return height;
}
public BlockHeader getBlockHeader() {
return blockHeader;
}
}
@@ -0,0 +1,58 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.io.Config;
import java.util.List;
import java.util.Locale;
public class NewWalletTransactionsEvent {
private final Wallet wallet;
private final List<BlockTransaction> blockTransactions;
private final long totalBlockchainValue;
private final long totalMempoolValue;
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
this.wallet = wallet;
this.blockTransactions = blockTransactions;
this.totalBlockchainValue = totalBlockchainValue;
this.totalMempoolValue = totalMempoolValue;
}
public Wallet getWallet() {
return wallet;
}
public List<BlockTransaction> getBlockTransactions() {
return blockTransactions;
}
public long getTotalValue() {
return totalBlockchainValue + totalMempoolValue;
}
public long getTotalBlockchainValue() {
return totalBlockchainValue;
}
public long getTotalMempoolValue() {
return totalMempoolValue;
}
public String getValueAsText(long value) {
BitcoinUnit unit = Config.get().getBitcoinUnit();
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit == BitcoinUnit.BTC) {
return CoinLabel.getBTCFormat().format((double) value / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
}
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
}
}
@@ -0,0 +1,28 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class OpenWalletsEvent {
private final Map<Wallet, Storage> walletsMap;
public OpenWalletsEvent(Map<Wallet, Storage> walletsMap) {
this.walletsMap = walletsMap;
}
public List<Wallet> getWallets() {
return new ArrayList<>(walletsMap.keySet());
}
public Storage getStorage(Wallet wallet) {
return walletsMap.get(wallet);
}
public Map<Wallet, Storage> getWalletsMap() {
return walletsMap;
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
public class PSBTCombinedEvent extends PSBTEvent {
public PSBTCombinedEvent(PSBT psbt) {
super(psbt);
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
public class PSBTEvent {
private final PSBT psbt;
public PSBTEvent(PSBT psbt) {
this.psbt = psbt;
}
public PSBT getPsbt() {
return psbt;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
/**
* This event is fired once the signing animation has finished and the PSBT has been finalized
*
*/
public class PSBTFinalizedEvent extends PSBTEvent {
public PSBTFinalizedEvent(PSBT psbt) {
super(psbt);
}
}
@@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
/**
* This event is used by the DeviceSignDialog to indicate that a USB device has signed a PSBT
*
*/
public class PSBTSignedEvent extends PSBTEvent {
private final PSBT signedPsbt;
public PSBTSignedEvent(PSBT psbt, PSBT signedPsbt) {
super(psbt);
this.signedPsbt = signedPsbt;
}
public PSBT getSignedPsbt() {
return signedPsbt;
}
}
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.event;
public class PagedEvent {
private final int pageStart;
private final int pageEnd;
public PagedEvent(int pageStart, int pageEnd) {
this.pageStart = pageStart;
this.pageEnd = pageEnd;
}
public int getPageStart() {
return pageStart;
}
public int getPageEnd() {
return pageEnd;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveActionEvent {
private final NodeEntry receiveEntry;
public ReceiveActionEvent(NodeEntry receiveEntry) {
this.receiveEntry = receiveEntry;
}
public NodeEntry getReceiveEntry() {
return receiveEntry;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveToEvent {
private final NodeEntry receiveEntry;
public ReceiveToEvent(NodeEntry receiveEntry) {
this.receiveEntry = receiveEntry;
}
public NodeEntry getReceiveEntry() {
return receiveEntry;
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestConnectEvent {
//Empty event class used to request programmatic connection to the server
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestDisconnectEvent {
//Empty event class used to request programmatic disconnection from the server
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestOpenWalletsEvent {
//Empty event class used to request a List of opened wallets
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestQRScanEvent {
//Empty event class used to request the QRScanDialog is opened
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestTransactionOpenEvent {
//Empty event class used to request the transaction open file dialog
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestWalletOpenEvent {
//Empty event class used to request the wallet open dialog
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import java.util.List;
public class SendActionEvent {
private final List<HashIndexEntry> utxoEntries;
public SendActionEvent(List<HashIndexEntry> utxoEntries) {
this.utxoEntries = utxoEntries;
}
public List<HashIndexEntry> getUtxoEntries() {
return utxoEntries;
}
}
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class SettingsChangedEvent {
private final Wallet wallet;
private final Type type;
public SettingsChangedEvent(Wallet wallet, Type type) {
this.wallet = wallet;
this.type = type;
}
public Wallet getWallet() {
return wallet;
}
public Type getType() {
return type;
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB;
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import java.util.List;
public class SpendUtxoEvent {
private final List<HashIndexEntry> utxoEntries;
public SpendUtxoEvent(List<HashIndexEntry> utxoEntries) {
this.utxoEntries = utxoEntries;
}
public List<HashIndexEntry> getUtxoEntries() {
return utxoEntries;
}
}

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