Compare commits

...
974 Commits
Author SHA1 Message Date
Craig Raw ad091217d6 set bip47 wallet from an associated wallet 2021-12-16 13:05:39 +02:00
Craig Raw 40e06b96a9 only follow paynym if bip47 wallet is loaded 2021-12-16 12:54:11 +02:00
Craig Raw 3fd186e22c v1.5.5 2021-12-16 11:00:54 +02:00
Craig Raw e8c7f57704 update seedsigner import description for 0.4.5 firmware 2021-12-16 10:58:21 +02:00
Craig Raw e6de33034b add seed tool as an airgapped hardware wallet 2021-12-16 10:06:35 +02:00
Craig Raw 73b6b9219b handle offline state when initiating a collaborative mix 2021-12-16 08:56:01 +02:00
Craig Raw d1d1b0806f fix about dialog text 2021-12-16 08:35:36 +02:00
Craig Raw 5ee97966ee upgrade to logback 1.2.8 2021-12-16 08:28:14 +02:00
Craig Raw 4819f329ae dont allow soroban mix to start if initiator is not connected 2021-12-15 16:45:40 +02:00
Craig Raw 52aed8a3f5 update nightjar to permit non-zero counterparty accounts in stowaways 2021-12-15 16:08:15 +02:00
Craig Raw 86ef129c1b follow up for theming 2021-12-15 14:28:31 +02:00
Craig Raw aff872eea0 show relative sizes of amounts in transaction diagram 2021-12-15 14:14:08 +02:00
Craig Raw 5d0025b4a7 allow positive amounts below dust limit when sending to paynyms 2021-12-15 09:42:30 +02:00
Craig Raw e625a4e542 remove account using tab context menu 2021-12-15 09:11:38 +02:00
Craig Raw 72ff1df61e add menu item to show paynym 2021-12-14 20:08:56 +02:00
Craig Raw 3776fbafd9 set txo label to payment label when sending multiple consolidation outputs 2021-12-14 18:53:48 +02:00
Craig Raw cd91aff3bb show entered labels in transaction view diagram when sending to multiple recipients 2021-12-14 18:24:37 +02:00
Craig Raw b530ced9ed support scanning crypto-account and crypto-output through both qr scans on settings tab 2021-12-14 11:21:32 +02:00
Craig Raw 22957e9d88 fix tx input issue 2021-12-13 16:23:46 +02:00
Craig Raw d76aecb34d handle quotes in connected device passphrase on windows 2021-12-13 14:27:35 +02:00
Craig Raw 4cbb402931 fix mempool size chart tooltip legend 2021-12-13 13:35:22 +02:00
Craig Raw 4da82b110c various minor fixes 2021-12-13 12:45:21 +02:00
Craig Raw 880096a193 v1.5.4 2021-12-10 08:07:20 +02:00
Craig Raw 06c0fb8594 add menu item to lock all open wallets in a window 2021-12-10 07:59:44 +02:00
Craig Raw 8e85543c6e fix module related build issue prevent qr scanner from opening 2021-12-10 07:57:56 +02:00
Craig Raw 4fb72fdf89 v1.5.3 2021-12-09 13:07:04 +02:00
Craig Raw cf2616ec2b show progress indicator when retrieving paynym 2021-12-09 12:04:53 +02:00
Craig Raw 238aae5ea9 follow up 2021-12-09 11:51:24 +02:00
Craig Raw a963d10381 change windows and linux installers to use sparrow menu group 2021-12-09 11:50:32 +02:00
Craig Raw eb90d6a31a constrain locktime datetimepicker to show valid values only 2021-12-09 10:28:33 +02:00
Craig Raw b013b5f50f add paging for batched server requests with configurable page size defaulting to 500 ids 2021-12-09 09:29:50 +02:00
Craig Raw 14db333a6f improve passphrase toggle of bitbox02 and trezor t 2021-12-08 14:39:16 +02:00
Craig Raw 6804f713b2 update hummingbird to v1.6.4 (revised crypto-account format) 2021-12-08 10:56:33 +02:00
Craig Raw 90a2c3b89b improve ux when validating transaction locktime datetime field 2021-12-08 10:20:21 +02:00
Craig Raw 3c94664ac3 update utxo tab ui to show utxo balance and count next to smaller utxo chart 2021-12-07 13:29:03 +02:00
Craig Raw 1b61a78e6d add minimum fee rates source of static 1 sat/vb for all block targets 2021-12-07 10:26:33 +02:00
Craig Raw 1defe51fd7 add button to view password field contents 2021-12-06 12:06:51 +02:00
Craig Raw 4cbd778ca1 show wallet display name on mix to button 2021-12-03 14:02:48 +02:00
Craig Raw e59ee47624 remember and select previously selected tab when closing a tab 2021-12-03 12:21:09 +02:00
Craig Raw 8b42399423 allow configuration of a maximum server timeout (maxServerTimeout) in sparrow config 2021-12-03 11:16:01 +02:00
Craig Raw e84f82f47b follow up 2021-12-03 10:45:23 +02:00
Craig Raw e4189711bd improve server connection and wallet loading pulse animation efficiency 2021-12-03 10:34:59 +02:00
Craig Raw 9bca911b0b show only unspent amount in status bar when refreshing postmix wallets 2021-12-02 14:24:09 +02:00
Craig Raw eb498f2bcc improve error handling for paynym.is connection issues 2021-12-02 13:33:30 +02:00
Craig Raw 3d13f596a0 v1.5.3-beta1 2021-12-01 16:07:33 +02:00
Craig Raw 28984427e1 upgrade to h2 2.0.202 2021-12-01 15:37:28 +02:00
Craig Raw ed92acc468 change default min mix to = 3, various minor ux improvements 2021-12-01 15:10:31 +02:00
Craig Raw 3b9c87abc7 improve incorrect password handling on final mix confirm 2021-12-01 14:43:48 +02:00
Craig Raw 00181875c1 followup 2021-12-01 14:14:13 +02:00
Craig Raw 26fb2b97fb add pay to paynym via payjoin 2021-12-01 14:11:16 +02:00
Craig Raw 44194a074c add and integrate paynym dialog 2021-11-30 14:43:23 +02:00
Craig Raw 4edd84f6e2 collaborative mix aesthetic tweaks 2021-11-29 17:08:58 +02:00
Craig Raw 0956c96046 integrate paynyms to collaborative mixing 2021-11-29 15:31:33 +02:00
Craig Raw 3013688447 trigger full wallet refresh when all transaction history has changed on loading 2021-11-26 14:05:56 +02:00
Craig Raw 0302913c3f create two person coinjoin transactions using soroban 2021-11-25 16:15:59 +02:00
Craig Raw 72768362a5 avoid concurrent modification error 2021-11-23 09:21:50 +02:00
Craig Raw 39fa65ea37 restart whirlpool if no utxos mixing, bind debug logging accelerator 2021-11-23 09:17:19 +02:00
Craig Raw 4554c9d0df catch and show hwi enumerate errors 2021-11-17 17:42:50 +02:00
Craig Raw 9b9b295045 force save of temp backup if refreshed wallet transactions are less 2021-11-17 17:13:41 +02:00
Craig Raw ece786131e check if wallet is mixing and restart whirlpool client if necessary 2021-11-17 16:02:11 +02:00
Craig Raw dc65313313 show wallet output descriptors with multipath indexes as per bitcoin core pr #22838 2021-11-13 15:05:45 +02:00
Craig Raw cb41a1ed66 fix import of encrypted json wallet on linux 2021-11-13 10:05:05 +02:00
Craig Raw c566dea232 support tr script expressions in qr crypto-output 2021-11-12 17:18:24 +02:00
Craig Raw 8f04d23b3f add utxo set replacement ui 2021-11-12 16:46:05 +02:00
Craig Raw b8b1039ada show utxo sets in transaction diagram 2021-11-12 15:54:49 +02:00
Craig Raw a7aafa27d0 indicate which accounts are scanned in info dialog 2021-11-12 10:07:53 +02:00
Craig Raw 18a1e82dda fix linux mix to label ellipsis 2021-11-11 18:08:56 +02:00
Craig Raw e26587e807 fix combobox texfield repeat selection 2021-11-11 15:32:37 +02:00
Craig Raw c3778b6419 fix version update hyperlink appearing multiple times 2021-11-10 16:13:23 +02:00
Craig Raw fb85277894 output hwi signing exception to log 2021-11-09 12:35:06 +02:00
Craig Raw 8ae3399d56 fix to avoid scanning with guide box 2021-11-08 11:49:17 +02:00
Craig Raw 7272de90f0 fix mix to wallet display name 2021-11-08 11:11:15 +02:00
Craig Raw 98c1f05ed6 double pass qr with wide and cropped image 2021-11-08 10:30:56 +02:00
Craig Raw 90439501ad improve qr scanning performance by cropping to indicated box 2021-11-07 13:04:20 +02:00
Craig Raw 1bb3833cbe reduce send tab chart min heights 2021-11-05 16:20:35 +02:00
Craig Raw d7d8140050 follow up 2021-11-05 16:01:23 +02:00
Craig Raw 3c77552211 show script type description when importing wallet keystore 2021-11-05 15:57:08 +02:00
Craig Raw 5e7d1d1f69 allow for minimum application height of 708px 2021-11-05 12:59:06 +02:00
Craig Raw 8f92f9ec38 upgrade to javafx 17 2021-11-04 15:19:43 +02:00
Craig Raw af23f063f2 show info dialog when no new accounts are discovered 2021-11-04 14:44:38 +02:00
Craig Raw 13a3ce23e7 make transaction diagram tooltips show indefinitely 2021-11-04 13:21:40 +02:00
Craig Raw d7ff73243c remember recent servers for quick reconfiguration 2021-11-04 11:04:48 +02:00
Craig Raw 25dd0440f6 add show transaction/psbt as qr from file menu 2021-11-03 14:09:49 +02:00
Craig Raw b5301c4556 add public testnet electrum server 2021-11-03 12:27:45 +02:00
Craig Raw a22f69e2c1 improve long fade out animation efficiency 2021-11-02 18:52:37 +02:00
Craig Raw 97f312cb93 v1.5.2 2021-10-29 15:47:48 +02:00
Craig Raw 269fd7f0da fix issue when displaying tx with new wallet 2021-10-29 15:43:52 +02:00
Craig Raw f0a813d031 handle concurrent modifications when saving address nodes 2021-10-29 14:39:51 +02:00
Craig Raw ceb5d85648 various minor fixes 2021-10-29 14:33:47 +02:00
Craig Raw 6b6b23b51a pay to the next fresh address of any open wallet via dropdown on send tab address field 2021-10-29 13:23:27 +02:00
Craig Raw 180e76f0f8 support discovery of singlesig connected hardware wallet accounts 2021-10-29 11:22:34 +02:00
Craig Raw d3b1c51115 increase toggle connecting animation rate 2021-10-28 16:55:24 +02:00
Craig Raw 72cb696451 add bip44 account discovery 2021-10-28 16:44:36 +02:00
Craig Raw 784fa5e1e8 minor whirlpool related fixes 2021-10-28 13:52:47 +02:00
Craig Raw 37aa3c9712 support account renaming, configuration of mix index range 2021-10-27 16:14:35 +02:00
Craig Raw 9520f6d218 upgrade to nightjar 0.2.19 (whirlpool client 0.23.37), minor tx diagram improvements 2021-10-27 12:01:04 +02:00
Craig Raw f4810bb568 show transaction diagram on every transaction headers screen 2021-10-26 17:35:58 +02:00
craigrawandGitHub febd5c33a2 Merge pull request #281 from grettke/master
Add mailmap file for Craig Raw addresses.
2021-10-25 13:47:20 +02:00
Craig Raw 4b39316821 recount mixes if mix data unavailable, correct mix status cell to remove mix progress from non-mixing utxos, show registered inputs total in tooltip 2021-10-25 12:25:18 +02:00
Grant Rettke 69544a2dc8 Add mailmap file for Craig Raw addresses. 2021-10-24 20:03:13 -05:00
craigrawandGitHub 2a0412320a Merge pull request #205 from RequestPrivacy/build
Add note to bitcoinbinary.org in build info
2021-10-22 11:49:09 +02:00
craigrawandGitHub fffd489bcf Merge pull request #278 from alaznem/master
Add other requirements for building to main README
2021-10-22 11:47:56 +02:00
craigrawandGitHub 70a6b259ba Merge pull request #267 from sashafklein/patch-1
Clarify how to run sparrow from CL
2021-10-22 11:43:50 +02:00
Craig Raw 26c177bd00 follow up 2021-10-22 11:19:26 +02:00
Craig Raw 1497b3d3bb add fee rate selection for premix 2021-10-22 11:01:58 +02:00
Alazne MoralesandGitHub 230f3c3db7 Add other requirements for building to main README
The "other requirements" are listed in the [reproducible builds readme](https://github.com/sparrowwallet/sparrow/blob/master/docs/reproducible.md#other-requirements) already. However when trying to quickly build the binaries and follow the main README instructions, the last paragraph about "[m]ore detailed instructions on reproducing the binaries are provided" can be overlooked very easily. Because the paragraph is about the specific topic "reproducible builds" and not building the binaries from source in general.
2021-10-21 20:39:50 +00:00
Craig Raw 813e0f3ab1 retrieve stored index from mixconfig on whirlpool wallet load 2021-10-21 14:38:13 +02:00
Craig Raw 237f97852d add coin selection filter to exclude immature coinbase outputs 2021-10-16 11:10:24 +02:00
Sasha KleinandGitHub 97ef580602 Clarify how to run sparrow from CL
This tiny Readme edit would, I think, have preempted the confusion that prompted #263 . I thought that the general install installed a global command, and didn't think, for some time, to run from within the directory. Dumb, but figured clarifying the doc would be better than just figuring it out silently on my own.
2021-10-14 19:35:37 -05:00
Craig Raw 2debc07375 import and export a wallet as an output descriptor in a text file 2021-10-14 14:27:13 +02:00
Craig Raw 1b3a35fda7 improve ui with whirlpool startup errors 2021-10-14 13:35:17 +02:00
Craig Raw 23fd597ca5 constrain mix from and mix to options to match those supported in the whirlpool client 2021-10-14 11:12:31 +02:00
Craig Raw eb012f638e fix popup window placement on moving active window back to primary screen 2021-10-14 10:23:04 +02:00
Craig Raw 63259a2056 fix lcd text rendering issue on osx 2021-10-14 09:55:26 +02:00
Craig Raw bad209ea5b automatically increase gap limit if required by postmix handler 2021-10-13 15:21:14 +02:00
Craig Raw 776fcb3044 permit mixing from badbank 2021-10-12 12:46:42 +02:00
Craig Raw 63ec856e87 improve transaction file opening io 2021-10-11 11:13:56 +02:00
Craig Raw ada8ca28e8 add tor proxy prompt text 2021-10-11 09:51:25 +02:00
Craig Raw ebd629db3a v1.5.1 2021-10-07 13:11:28 +02:00
Craig Raw c18a2f4388 improve tor identity management 2021-10-07 12:23:28 +02:00
Craig Raw 6f95dbe309 support bitcoin core connections over tor 2021-10-06 20:37:58 +02:00
Craig Raw 576253e651 fix remaining issue when utxo date is null 2021-10-06 10:08:56 +02:00
Craig Raw b9d6cb17d4 indicate when entered seed is of unsupported electrum type 2021-10-06 08:23:08 +02:00
Craig Raw 3b730a1711 preserve check menu item state across windows 2021-10-05 16:38:56 +02:00
Craig Raw 8c0a1932cf add prevent computer sleep functionality 2021-10-05 16:16:32 +02:00
Craig Raw 35b57f9d69 add txid to transactions csv download 2021-10-05 10:55:20 +02:00
Craig Raw d6ad7f4808 handle null dates in date cell 2021-10-05 10:39:11 +02:00
Craig Raw 3e47a49f49 core connection: default to cookie authentication when user/pass are not filled in 2021-10-05 10:08:51 +02:00
Craig Raw 4c817d243d fix long specter desktop keystore labels 2021-10-04 14:45:02 +02:00
Craig Raw 22b7b659f3 include amount in transaction pie chart tooltip 2021-10-04 14:44:19 +02:00
Craig Raw 9dd6068e69 make temp backup permanent when created in a previous process 2021-10-04 13:49:41 +02:00
craigrawandGitHub 54baee57e1 Merge pull request #235 from romanz/master
Allow running `sparrow` script from other paths
2021-10-04 12:54:48 +02:00
Roman Zeyde 581aaf288e Allow running sparrow script from other paths
IIUC, it currently assumes that it's run locally using `./sparrow`.
2021-10-04 13:44:42 +03:00
Craig Raw 2fa47e640d various whirlpool-related improvements 2021-10-04 12:16:03 +02:00
Craig Raw ea03dece72 add lock wallet functionality 2021-10-01 15:47:01 +02:00
Craig Raw 8e0b9a3ea0 ask for passphrase re-entry when creating a bip39 wallet 2021-09-30 12:49:03 +02:00
Craig Raw 67179127e3 minor whirlpool related fixes 2021-09-30 10:52:40 +02:00
Craig Raw 4ebee8a8f3 dont allow double click to receive on whirlpool wallet 2021-09-29 10:41:16 +02:00
Craig Raw 2548e77d90 followup to disable adding accounts to p2sh legacy multisig 2021-09-29 10:32:43 +02:00
Craig Raw 58e3b9dcdd add multiple account functionality 2021-09-29 10:11:51 +02:00
Craig Raw 429b733140 prevent setting separate passwords on child wallets 2021-09-27 13:24:29 +02:00
Craig Raw 56e3a54ae0 clarify and improve wallet password and keystore passphrase entry 2021-09-27 12:49:46 +02:00
Craig Raw a934ffa76c fix issues when removing selected items from utxotreetable 2021-09-27 11:46:50 +02:00
Craig Raw 86a49e0d9a fix npe showing the mix to dialog when non-standard wallets are loaded 2021-09-27 10:08:48 +02:00
Craig Raw 761e9c9b7e use wallet unit when displaying pool denomination 2021-09-27 09:33:41 +02:00
Craig Raw 712241873f show reason for mix error in tooltip 2021-09-27 08:54:42 +02:00
Craig Raw 31f9cca33a handle electrs batching detection better 2021-09-27 08:15:20 +02:00
Craig Raw 395e90e2a5 undo #181: always create bitcoin core wallet if missing 2021-09-27 08:14:35 +02:00
Craig Raw b2657cdcfb v1.5.0 final 2021-09-23 15:52:33 +02:00
Craig Raw 6bbae204a6 restrict mixing to mainnet and testnet 2021-09-23 15:30:13 +02:00
Craig Raw 0b55dd8a1e ui fixes to mix start and stop 2021-09-23 14:18:37 +02:00
Craig Raw f74287697c use master wallet passphrase for child wallets when loading 2021-09-23 12:49:06 +02:00
Craig Raw 7a3e1dfa1f change default log level to warn 2021-09-23 12:28:47 +02:00
Craig Raw 427a6925ee mainnet mixing, improve pools selection, other fixes 2021-09-23 10:59:40 +02:00
RequestPrivacy 90dea201b9 Add note to bitcoinbinary.org in build info
Actively encourage people to document their build process and provide their work back to the community. Further, push adoption of bitcoinbinary.org as a site for verification and establish sparrow as a well proven/verified wallet.
2021-09-22 15:38:49 +02:00
Craig Raw c55b19af0f tor status windows visual fixes 2021-09-21 11:55:23 +02:00
Craig Raw e12f7a634a add tor status indicator to status bar 2021-09-21 11:22:09 +02:00
Craig Raw ca1f934138 update README for https cloning 2021-09-20 16:42:55 +02:00
Craig Raw 4f9b87b74e fix recursive clone via https 2021-09-20 16:37:03 +02:00
Craig Raw e1e5df78c6 fix text truncation in whirlpool dialog on windows 2021-09-20 15:51:57 +02:00
Craig Raw cfd06a8513 use cached tx0previews, only save mixconfig on apply 2021-09-20 15:30:13 +02:00
craigrawandGitHub 1c1099217b Merge pull request #198 from zeroleak/whirlpool-client-0.23.30-early5
update to whirlpool-client 0.23.31
2021-09-20 13:10:47 +02:00
zeroleak 6939d8a06a upgrade to whirlpool-client 0.23.33 2021-09-18 11:41:52 +02:00
zeroleak dbebade3ab update to whirlpool-client 0.23.31 2021-09-11 08:17:15 +02:00
Craig Raw 5895837b60 hide receive tab on whirlpool wallets 2021-09-10 15:35:40 +02:00
Craig Raw 0f9e9d9c35 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-09-10 13:58:47 +02:00
Craig Raw 59452e64ea add additional required packages 2021-09-10 13:58:18 +02:00
craigrawandGitHub 573d5b2c18 Merge pull request #195 from RequestPrivacy/patch-1
Fix skdman link
2021-09-10 10:01:47 +02:00
Craig Raw 2f0dc5bea8 ensure submodule is also on correct tag 2021-09-10 09:41:30 +02:00
RequestPrivacyandGitHub 27f7d5ad38 Fix skdman link 2021-09-09 16:16:54 +00:00
Craig Raw 414ffc2648 add first draft of reproducible build documentation 2021-09-09 14:07:45 +02:00
Craig Raw aaca9ffa16 dont allow receiving on badbank wallet, introduce skipInstaller flag in build 2021-09-08 13:22:21 +02:00
Craig Raw 44b9cee825 fix outdated reference to Java 14 and link to Java 16 JDK 2021-09-07 19:27:22 +02:00
Craig Raw c024c351ac minor fixes 2021-09-07 11:51:14 +02:00
Craig Raw ed2914f2aa replace illegal filename characters in wallet name with underscores 2021-09-06 14:49:53 +02:00
Craig Raw 0b4785e01c v1.5.0 beta1 2021-09-06 11:53:34 +02:00
Craig Raw 0c50c9cb9c make jackson jars open and non-synthetic modules 2021-09-06 10:18:36 +02:00
Craig Raw 916c2b6122 fix module issues in binary 2021-09-05 15:47:15 +02:00
Craig Raw 308a89c958 update required java to 16 in build instructions 2021-09-03 20:51:56 +02:00
Craig Raw 88ebef97d4 support mixing from all single sig wallets, handle tor proxy change, and other minor fixes 2021-09-03 17:16:37 +02:00
Craig Raw a42761981c support mixing to multisig wallets 2021-09-02 17:14:01 +02:00
Craig Raw b6f047d382 minor fixes 2021-09-02 15:26:16 +02:00
Craig Raw 8f63d89be8 avoid creating a core wallet if no wallets are open 2021-09-02 15:22:53 +02:00
Craig Raw 6e6111b47c support using stored mix indexes to avoid resending a utxo to the coordinator 2021-09-02 14:12:59 +02:00
Craig Raw e8af7c70bd refactor to WhirlpoolServices 2021-09-02 12:37:07 +02:00
Craig Raw 2fc551e35b add mix to functionality 2021-09-02 11:39:56 +02:00
Craig Raw adb77771aa add mix config persistence and initial usage 2021-09-01 13:10:46 +02:00
Craig Raw 13e01451b7 update nightjar for tor identity change functionality 2021-08-31 16:43:56 +02:00
Craig Raw aa10bcfe1a fixes for encrypted whirlpool wallets and other issues 2021-08-31 16:19:24 +02:00
craigrawandGitHub f30c00ba8f Merge pull request #183 from zeroleak/whirlpool-client-0.23.30-early4
Upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3
2021-08-31 10:57:49 +02:00
zeroleak 4577a64ad5 apply Craig's feedback
- use V2__Whirlpool.sql
- allow pools list & tx0 preview without seed
- filter DataSource wallet for refresh
2021-08-29 10:49:25 +02:00
Craig Raw 7371ca2994 add option to optimize transactions for privacy and display privacy analysis 2021-08-27 16:00:17 +02:00
zeroleak 050c4fc31e upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-27 09:20:41 +02:00
zeroleak 772370808c Merge remote-tracking branch 'remotes/origin/master' into whirlpool-client-0.23.30-early4
# Conflicts:
#	src/main/java/com/sparrowwallet/sparrow/AppServices.java
#	src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java
2021-08-24 12:18:54 +02:00
Craig Raw b22e891b7d make select all, copy on transaction hex area copy untruncated transaction hex 2021-08-24 10:52:56 +02:00
Craig Raw 57b3214c54 improvements to whirlpool dialog 2021-08-24 10:40:27 +02:00
Craig Raw 615b78b497 fix mix event handling for multiple wallets 2021-08-24 09:29:54 +02:00
Craig Raw d1ab1db1c5 fix issues with subtabs in new wallets 2021-08-23 17:27:30 +02:00
Craig Raw f5ac6a3b73 support whirlpool data storage in wallet file, add mixing ui 2021-08-23 16:36:02 +02:00
zeroleak fec45356a2 upgrade to whirlpool-client 0.23.30-early4 + extlibj 0.0.19-dsk3 2021-08-22 11:26:11 +02:00
Craig Raw 37c4ff4dd7 reduce max number multisig keystores 2021-08-16 17:09:21 +02:00
Craig Raw b0877d94bf make all wallet addresses non-editable once child wallets are added 2021-08-16 13:33:05 +02:00
Craig Raw a3e4342d7d save app width and height across restarts 2021-08-16 11:25:18 +02:00
Craig Raw 90355a653f followup 2021-08-16 11:04:46 +02:00
Craig Raw 6339c3a7d7 allow multisig wallets with many (up to 50) keystores 2021-08-16 10:46:34 +02:00
Craig Raw 8f7f0d4c61 truncate labels over 255 chars before persisting to db 2021-08-16 10:15:14 +02:00
Craig Raw 74b4f51640 add menu items to copy psbt to clipboard in hex or base64 2021-08-16 08:48:14 +02:00
Craig Raw 2caee79df4 initial whirlpool integration 2021-08-12 17:50:13 +02:00
Craig Raw 34b4c39ccd add logging when event wallet id is not present 2021-08-04 10:45:01 +02:00
Craig Raw 34d1571669 fix aopp verification regression on button type 2021-08-04 10:03:18 +02:00
Craig Raw 22465b1b65 followup 2021-08-02 16:10:47 +02:00
Craig Raw b91e8eab51 fix tab loading animation for subtabbed wallets 2021-07-30 15:13:58 +02:00
Craig Raw 55e69bf263 fix naming when using a subtab wallet, import and export wallets with child wallets with db persistence on sparrow exporter 2021-07-30 14:17:21 +02:00
Craig Raw 02d3817cb1 introduce wallet subtabs 2021-07-30 09:46:42 +02:00
Craig Raw fc9cdaabb4 export UTXOs to CSV 2021-07-29 12:09:54 +02:00
Craig Raw 2aa3d83402 fix using the correct address label for the utxo label when recieving a batched tx to several labelled address 2021-07-29 11:40:15 +02:00
Craig Raw fc5d6ada36 add caching for verbose transaction lookups to avoid repeat server requests 2021-07-29 11:09:49 +02:00
Craig Raw be599fb003 fix hummingbird module definition 2021-07-21 15:30:46 +02:00
Craig Raw d79c4a13f6 upgrade to hummingbird v1.6.2 2021-07-20 11:47:56 +02:00
Craig Raw e6fce14fde handle unknown derivation path for unchained caravan key 2021-07-19 11:56:02 +02:00
Craig Raw 5c3a00b71b v1.4.3 2021-07-15 12:02:45 +02:00
Craig Raw ced4d4d337 add caravan import/export, minor ui fixes 2021-07-15 11:09:39 +02:00
Craig Raw 4a3ad9f4ff restrict shown sighash to available values 2021-07-14 15:13:38 +02:00
Craig Raw f5a72105ac minor fixes after refactoring 2021-07-12 08:56:37 +02:00
Craig Raw 0502eec0cd dont show message sign context menu item when message signing cant be performed 2021-07-09 13:35:45 +02:00
Craig Raw 8e6933b5ca increase animation rate and fragment length for UR QRs 2021-07-09 08:23:40 +02:00
Craig Raw 1fd1dec6cf rename tx segwit version field to segwit flag 2021-07-08 11:46:21 +02:00
Craig Raw 422713ff53 fix camera selection issues on linux 2021-07-05 12:58:26 +02:00
Craig Raw ada45ee75b allow selection of webcam from QR scan dialog 2021-07-05 12:33:31 +02:00
Craig Raw 2f153686dd avoid hang on closing webcam due to rescans for new camera devices 2021-07-04 19:18:02 +02:00
Craig Raw f691f1691e add missing requires directive for bwt lib 2021-07-02 13:41:22 +02:00
Craig Raw 1f9e37b40c enable max button for selected utxos without address and label filled 2021-07-02 12:05:38 +02:00
Craig Raw a1d2de1859 use all addressable script types (not just single hash types) 2021-07-02 10:44:24 +02:00
Craig Raw 143472bdfc fix save of address labels on a new wallet 2021-07-01 09:41:56 +02:00
Craig Raw b9e64d42ff accept pasting a whitespace delimited sequence of words into
a textfield of mnemonic word entry
2021-06-30 11:55:58 +02:00
Craig Raw 9a09bb8cda always delete hwi dir on osx before copying 2021-06-30 11:51:31 +02:00
Craig Raw 4b028af123 handle multiple selection when freezing and unfreezing utxos 2021-06-30 09:18:14 +02:00
Craig Raw 8033e5fd88 improve amount error labels adding dust threshold label for too low amounts 2021-06-29 12:48:02 +02:00
Craig Raw badf8c8f2f optimize and increase sampling rate of qr reading 2021-06-29 10:53:22 +02:00
Craig Raw b6a353815c switch mempool size chart to kvB when max Y value is less than 1 MvB 2021-06-28 14:14:22 +02:00
Craig Raw ea2f858dc9 close message sign dialog on platform button and escape key 2021-06-28 13:32:24 +02:00
Craig Raw 324540009a fix detection of input type on finalizing psbt wallet 2021-06-28 13:08:44 +02:00
Craig Raw 1c1f90344f update github build to java 16 2021-06-25 15:18:51 +02:00
Craig Raw 094dd45547 upgrade to gradle 7.1, java 16, javafx 16 2021-06-25 14:56:22 +02:00
Craig Raw 6d434722cc fix prev commit 2021-06-24 12:41:32 +02:00
Craig Raw c8a4ed0c3d add documentation link to bitcoin core error message 2021-06-24 10:07:54 +02:00
Craig Raw c8d997fbf0 v1.4.2 2021-06-23 12:51:38 +02:00
Craig Raw 911ed3a718 improve background text 2021-06-23 12:30:58 +02:00
Craig Raw dbfed31432 add format toggle to message signing dialog (electrum or trezor) 2021-06-22 16:00:15 +02:00
Craig Raw 6f3d4e224e add context menu item to copy transaction hex 2021-06-22 11:47:56 +02:00
Craig Raw 4d6609990c fix error messages on subclassed importers/exporters 2021-06-22 10:13:37 +02:00
Craig Raw 5482196cc7 dark theme improvements 2021-06-22 08:36:16 +02:00
Craig Raw 09f6c9ef81 ensure cleanup of migrated wallets when importing 2021-06-21 11:50:58 +02:00
Craig Raw 9b8f97c041 fix import of sparrow wallet with seed, alphabetically sort import and export choices 2021-06-21 11:25:47 +02:00
Craig Raw c68c713a4b add orig files for keystone and seedsigner 2021-06-18 13:31:08 +02:00
Craig Raw 02e144f802 set keystore label, model and source type when importing a wallet from specter desktop 2021-06-18 11:21:53 +02:00
Craig Raw a9ab4d6c78 Handle importing a wallet from a crypto-account QR using the File > Import Wallet dialog 2021-06-17 14:34:11 +02:00
Craig Raw 5df4e5761c add seedsigner keystore import 2021-06-16 14:46:55 +02:00
Craig Raw 6d8b8579ba followup 2021-06-16 09:37:35 +02:00
Craig Raw eaa5190502 various db persistence improvements 2021-06-16 09:35:26 +02:00
Craig Raw 445db6a4d6 reduce file reads on db files to avoid locking exception on windows 2021-06-15 17:58:25 +02:00
Craig Raw 7f178b5f67 jpms related changes for v1.4.2-beta 2021-06-15 16:50:05 +02:00
Craig Raw 1208baf00e use mempool.space onion address for fee rates if tor proxy enabled 2021-06-15 09:31:12 +02:00
Craig Raw 655a473cd5 update send selected button when freezing or unfreezing utxos 2021-06-14 16:58:05 +02:00
Craig Raw e6c536930b highlight default button and allow actioning from keyboard when only one usb device is listed 2021-06-14 16:55:01 +02:00
Craig Raw f1510de360 update encrypted seeds and private keys when wallet password changes 2021-06-14 14:54:40 +02:00
Craig Raw cfac2768ae use varbinary rather than fixed binary column types where length is variable 2021-06-14 11:52:12 +02:00
Craig Raw ab41f2e80e upgrade to h2 HEAD 2021-06-11 15:45:16 +02:00
Craig Raw 9ebabecfbe use json persistence for sparrow wallet export, add sparrow wallet import to wallets dir 2021-06-11 11:29:54 +02:00
Craig Raw 8914acff68 minor followup 2021-06-10 17:02:32 +02:00
Craig Raw 4a0ecba716 add keystone hww import and export 2021-06-10 16:37:41 +02:00
Craig Raw e99b1d4171 upgrade to hwi 2.0.2 and relocate hwi to sparrow home folder on osx to avoid partial installation deletions in tmpdir 2021-06-10 14:06:14 +02:00
Craig Raw a59d5d3086 introduce database persistence with automatic migration of existing wallets 2021-06-10 12:08:35 +02:00
Craig Raw 600a77da3a allow psbts without utxo data to be loaded if utxos are provided in an existing psbt 2021-05-28 11:09:23 +02:00
craigrawandGitHub bc83f6fa22 Merge pull request #134 from haakonn/mnemonic-entry
Make mnemonic entry more efficient
2021-05-27 16:18:39 +02:00
Haakon Nilsen 4cbde7e7aa When entering mnemonic words, don't close dropdown when a prefix is encountered, and move focus to the next field upon completion 2021-05-27 10:56:47 +02:00
craigrawandGitHub 3ae63408e6 Merge pull request #133 from haakonn/save-tx-menuitems
Fixes around transaction saving menu items
2021-05-27 09:58:11 +02:00
Haakon Nilsen e740c6d162 Disable transaction saving menu items after a transaction tab is closed and no other tabs are open 2021-05-27 09:15:36 +02:00
Haakon Nilsen f7f5852476 Disable the "Save transaction" menu item when starting app without any open tabs 2021-05-26 18:57:07 +02:00
Haakon Nilsen 447e2ab264 Disable the keyboard shortcut for saving PSBT binary when its parent menu is disabled 2021-05-26 18:54:20 +02:00
craigrawandGitHub 8a77f22158 Merge pull request #132 from haakonn/close-on-escape
Close "About" and "Introduction" when Escape key is pressed
2021-05-26 08:39:16 +02:00
Haakon Nilsen c096327be4 Close "About" and "Introduction" when Escape key is pressed 2021-05-25 22:13:51 +02:00
Craig Raw 911153e1aa fix receive to address context menu actions 2021-05-25 08:21:17 +02:00
Craig Raw a60eadf8fc add export to specter diy 2021-05-24 14:20:08 +02:00
Craig Raw 9ebbf2557f support electrs batching and improve batching read timeout handling 2021-05-24 13:39:06 +02:00
Craig Raw 42b279d22a add specter desktop reimport usb wallets warning 2021-05-24 12:29:57 +02:00
Craig Raw 1a452db4cf add tooltip to send utxos button with directions on how to select multiple utxos 2021-05-24 12:27:30 +02:00
Craig Raw c1cf5be616 add mempool.bisq.services as a broadcaster, broadcast tx twice if possible on mainnet, handle different network broadcaster network capabilities 2021-05-21 11:48:13 +02:00
Craig Raw dd146210ba set send amount to total utxo value when sending selected utxos before an address or label is added 2021-05-21 09:53:00 +02:00
Craig Raw 94088f795c handle invalid values from mempool.get_fee_histogram 2021-05-21 08:44:55 +02:00
Craig Raw c5b09189df explicitly place decorationpane in the scene graph to avoid app resizing issues 2021-05-20 17:27:33 +02:00
Craig Raw 197c44bb07 update null and empty labels, clear script hash cache on increasing gap limit 2021-05-20 14:49:16 +02:00
Craig Raw c202a941b9 add mempool.emzy.de as another broadcaster 2021-05-20 14:48:01 +02:00
Craig Raw 383594b03c v1.4.1 2021-05-20 12:32:15 +02:00
Craig Raw 42bfe572ef calculate and cache script hashes statuses on wallet load to avoid unnecessary initial history fetching 2021-05-20 09:52:02 +02:00
Craig Raw 1677c47500 request payjoin transactions (over tor if available) in background thread 2021-05-19 12:24:46 +02:00
Craig Raw 53a447c72d fix cpu usage on spreadsheetview 2021-05-19 09:51:55 +02:00
Craig Raw f23a891ece add send to many dialog 2021-05-19 08:41:15 +02:00
Craig Raw 0e42c657b3 broadcast transactions over tor to a broadcasting service where tor proxy available 2021-05-18 09:37:59 +02:00
Craig Raw b17c15f702 request new change address in the transaction diagram 2021-05-17 13:21:56 +02:00
Craig Raw c9cdf6e77d handle partial rbf, fix cursor reset on payment label 2021-05-17 11:58:12 +02:00
Craig Raw 4f6ead842d fix txtree selection bug when preselecting a txi/txo on opening the editor for the first time 2021-05-17 10:53:26 +02:00
Craig Raw adcddfa84d various minor utxo screen improvements, including hiding the utxo chart 2021-05-17 10:09:19 +02:00
Craig Raw d67c5c5218 update related address, tx, txi and txo labels when null, set tx, txi, txo labels to address label on receive, add paste label to context menu 2021-05-14 18:03:46 +02:00
Craig Raw fb72010bdf refactor storage to handle different persistence strategies 2021-05-14 12:55:12 +02:00
Craig Raw 8bc8bdb2f2 dont backup wallets outside of wallets folder 2021-05-12 14:52:19 +02:00
Craig Raw 8f4cf9f2a0 add unique4j classes to avoid module dependency issue 2021-05-12 14:28:56 +02:00
Craig Raw ed323a8388 ensure aopp window is always on top 2021-05-12 09:19:49 +02:00
Craig Raw d559b437b3 bring window to front one external file load 2021-05-12 09:02:39 +02:00
Craig Raw 6155306acc always try obtain application lock on startup 2021-05-12 08:46:24 +02:00
Craig Raw 6f35d86890 bring opened file window to foreground 2021-05-11 12:58:11 +02:00
Craig Raw cbf32a36c6 follow up #2 2021-05-11 12:40:20 +02:00
Craig Raw 9789e0f198 follow up 2021-05-11 12:33:33 +02:00
Craig Raw 574209c837 ensure file and uri opening on original app instance 2021-05-11 12:31:36 +02:00
Craig Raw 74c83fc5e1 fade animation on server toggle while connecting 2021-05-11 08:52:45 +02:00
Craig Raw a4ddc11bab Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-05-10 14:34:57 +02:00
Craig Raw 3bf480234f fix aopp alert dialog on windows 2021-05-10 14:25:16 +02:00
Craig Raw 29811ca5de handle uri and psbt opening on linux 2021-05-10 14:22:24 +02:00
Craig Raw b7522420cf cleanup windows install 2021-05-10 13:41:46 +02:00
Craig Raw f7d1e22e4e use custom wix installation file 2021-05-10 13:39:40 +02:00
Craig Raw e0c38031b1 add aopp registry keys on windows install 2021-05-10 12:17:52 +02:00
Craig Raw e046512e86 open files or uris from commandline arguments 2021-05-10 11:44:06 +02:00
Craig Raw 7cba0de268 check if opening uris is supported 2021-05-10 08:42:50 +02:00
Craig Raw 425e476f20 add aopp proof of address ownership support 2021-05-07 16:24:28 +02:00
Craig Raw e5dd33d5a1 register and add bitcoin: uri handler, register aopp: handler 2021-05-06 14:21:56 +02:00
Craig Raw 07012615ff 2nd fix to alert height 2021-05-05 14:59:53 +02:00
Craig Raw 23f3ae51db hide alert on clicking link 2021-05-05 14:43:33 +02:00
Craig Raw 10514b8039 shorten mitm message to avoid alert height ellipsis 2021-05-05 14:35:43 +02:00
Craig Raw 19637fd706 handle missing webcam dependency on windows showing error alert with link to resolve 2021-05-05 14:31:28 +02:00
Craig Raw dd56e2b42e handle windows with owner only file and directory creation 2021-05-05 12:05:48 +02:00
Craig Raw e0b6530275 only save tmp backup wallet if connected 2021-05-05 11:34:45 +02:00
Craig Raw a0d3f3b745 ensure config, wallet dirs and wallets are owner read/write only 2021-05-05 11:25:25 +02:00
Craig Raw 438c13fe2d add wallet transactions loading log viewer, and improve loading messages 2021-05-04 15:17:57 +02:00
Craig Raw c067300ee8 fix empty addresses table bug after requesting many new addresses 2021-05-04 10:42:18 +02:00
Craig Raw 020ca2f312 always use 8 fraction digits when displaying btc amounts, use a fixed width font for numbers on osx 2021-05-04 10:23:35 +02:00
Craig Raw 5ede1dd97d ensure wallet export file streams are always closed 2021-04-30 14:03:58 +02:00
Craig Raw 45f6ae214a add icons to wallet context menu items where they appear as action buttons 2021-04-30 13:19:53 +02:00
Craig Raw 45a9093055 ensure import file stream is closed 2021-04-29 20:58:04 +02:00
Craig Raw d6c7a0b757 cache fetched script hash status and dont retrieve history again until it changes 2021-04-29 13:21:52 +02:00
Craig Raw 55181165c8 use read timeout to improve disconnection behaviour, especially while refreshing a wallet 2021-04-29 08:02:56 +02:00
Craig Raw b6f7483bf9 v1.4.0 2021-04-28 13:10:06 +02:00
Craig Raw 0593c764b6 remove socket read timeout 2021-04-28 13:03:58 +02:00
Craig Raw 11a3bfd893 make welcome dialog server toggle uneditable and add tooltip to indicate demonstration only 2021-04-28 12:33:18 +02:00
Craig Raw f1d7fc656c set mnemonic enter words button to number of chosen words when dropdown item clicked 2021-04-28 12:18:35 +02:00
Craig Raw ec918b78ed followup to public server change on wallet history failure 2021-04-28 11:46:46 +02:00
Craig Raw 6f5ee7c695 handle server error conditions better, and if using public server try another 2021-04-28 11:23:17 +02:00
Craig Raw e4264d0199 support toggling trezor passphrase on and off in status bar usb menu 2021-04-27 13:56:32 +02:00
Craig Raw 9feefe8203 show file open/save dialogs on the same screen as the currently focussed window 2021-04-27 12:52:42 +02:00
Craig Raw 93f9539f7b show dialogs on the same screen as the currently focussed window 2021-04-27 12:22:39 +02:00
Craig Raw 8865f4946a follow up to previous commit 2021-04-27 09:44:56 +02:00
Craig Raw 65e13d7b50 fix adding new payment tabs, update last used label on connection 2021-04-27 08:17:30 +02:00
Craig Raw 11189bc605 create wallet transaction in background thread to keep ui responsive, and indicate if long running 2021-04-26 16:45:17 +02:00
Craig Raw 40faaec31e handle user defined fee amount better by requiring a fee rate of 1 sat/vb for utxo selectors 2021-04-26 14:40:11 +02:00
Craig Raw b5b384f5da update transaction tree with final txid when transaction is extracted 2021-04-24 09:04:28 +02:00
Craig Raw 23c802bb5f cpfp follow up 2021-04-24 08:40:48 +02:00
Craig Raw 4567f62ef8 hide show derivation link to fix linux width issue 2021-04-23 12:16:20 +02:00
Craig Raw cd00535212 add create cpfp transaction functionality 2021-04-23 12:05:30 +02:00
Craig Raw 3ff626e2aa always set server toggle tooltip on connection to handle bwt startup 2021-04-22 12:24:09 +02:00
Craig Raw deee63c701 dont revert to tx broadcast state when verbose tx fetch fails 2021-04-22 12:12:42 +02:00
Craig Raw b465b9fdaf various minor ui fixes including update screens after includemempooloutputs changed 2021-04-21 13:09:31 +02:00
Craig Raw f377f97931 add submit bug report to help menu 2021-04-21 10:37:41 +02:00
Craig Raw 58bee33383 always display fresh unused address on startup and when address has been used 2021-04-21 10:23:48 +02:00
Craig Raw 18075e0686 improve variable naming for utxo selection criteria 2021-04-21 09:37:19 +02:00
Craig Raw b836774289 if allow unconfirmed, include mempool outputs in balance #2 2021-04-20 11:57:22 +02:00
Craig Raw f2f15208bf if allow unconfirmed, include mempool outputs in balance 2021-04-20 11:55:43 +02:00
Craig Raw 164742387f disable receive address field once used 2021-04-20 11:46:36 +02:00
Craig Raw e62b44bbbe allow external unconfirmed utxos to be spent 2021-04-20 11:08:44 +02:00
Craig Raw e9d0491b42 move insufficient funds label to below amount field 2021-04-20 10:08:07 +02:00
Craig Raw 3fc2127337 import and support master private key keystores 2021-04-20 08:28:16 +02:00
Craig Raw 702d92d4f2 handle failure to retrieve broadcasted transaction from mempool, allow retry 2021-04-19 10:34:47 +02:00
Craig Raw a440c22455 deduct unconfirmed transactions with negative values from balance 2021-04-19 09:50:09 +02:00
Craig Raw 1c2e635650 spacing followup 2021-04-19 09:34:28 +02:00
Craig Raw 7b856f32a2 add insufficient inputs feedback label 2021-04-19 09:29:19 +02:00
Craig Raw 42b96e042b improve bip39 keystore import ux 2021-04-19 08:41:46 +02:00
Craig Raw f8023e9a72 add (non-final) suffix to fee rate field when tx is lacking signatures 2021-04-16 14:05:53 +02:00
Craig Raw 7b9b78684c show tx hex tooltip input, output and witness indexes 2021-04-16 14:00:12 +02:00
Craig Raw 10d6fb8b2b fix npe on fee rates selection toggle 2021-04-16 12:30:59 +02:00
Craig Raw 1556b8930c null-safe testing of device needspin and needspassphrase 2021-04-16 11:38:01 +02:00
Craig Raw d52bade085 various minor ui improvements 2021-04-16 10:53:05 +02:00
Craig Raw 2c98c8606c add transaction hex tooltips 2021-04-15 15:18:00 +02:00
Craig Raw a8430c79eb allow manual configuration of hwi executable path 2021-04-14 11:25:40 +02:00
Craig Raw 3fe5fd89c4 fix qr display dialog sizing 2021-04-14 10:20:23 +02:00
Craig Raw 78406b3110 use cryptopsbt encoding 2021-04-14 09:17:42 +02:00
Craig Raw 2dfdbd6d78 support ur:crypto-output scan and display of wallet output descriptor 2021-04-13 17:27:42 +02:00
Craig Raw 2e86840e92 reduce opacity for unconfirmed txes 2021-04-12 16:27:10 +02:00
Craig Raw 9600d5707c when importing electrum wallets retrieve tx labels from address labels 2021-04-12 16:16:07 +02:00
Craig Raw 497cf333b0 ensure wallet cannot have negative balance with same block tx ordering 2021-04-12 15:47:45 +02:00
Craig Raw eb55b9420a add option to strip sensitive info from psbt serialization 2021-04-12 13:42:36 +02:00
Craig Raw 3b3e46983f deduplicate output descriptors for bwt 2021-04-12 12:22:51 +02:00
Craig Raw 31fb527218 color transaction values which deduct from wallet balance in red 2021-04-12 09:16:49 +02:00
Craig Raw 685fef6b76 shorten labels for linux 2021-04-08 09:20:35 +02:00
Craig Raw 9a4eb22396 v1.3.2 2021-04-08 08:38:16 +02:00
Craig Raw 57eb3a77e7 reduce error logging when fee and exchanges rate sources are unavailable 2021-04-08 08:34:31 +02:00
Craig Raw dcc63046bd switch to secure openimaj repo 2021-04-07 15:56:10 +02:00
Craig Raw 8388a7fed5 reduce camera cpu usage through sleeping webcam capture threads to match 10fps 2021-04-07 13:34:06 +02:00
Craig Raw d5830399b7 upgrade to hwi 2.0.1 2021-04-07 11:50:52 +02:00
Craig Raw 38b8fa3b86 add view menu item to switch hd capture 2021-04-07 10:36:18 +02:00
Craig Raw 6f26c7e138 update passport import descriptions 2021-04-07 10:01:44 +02:00
Craig Raw 9e869ae69b handle no camera detected better 2021-04-07 09:56:51 +02:00
Craig Raw 4a7b0b20ad tray changes for windows 2021-04-07 09:45:52 +02:00
Craig Raw 8fc971c07c add minimize to system tray functionality 2021-04-07 09:32:01 +02:00
Craig Raw cb884d97cb request strongest instance of securerandom when generating a seed 2021-04-06 11:46:51 +02:00
Craig Raw 942db9df48 edit passport wallet export description 2021-04-06 09:34:05 +02:00
Craig Raw 193f88b88f implement changes to export addresses functionality 2021-04-06 09:27:51 +02:00
craigrawandGitHub 6785bccf0e Merge pull request #89 from jimbojw/export-addresses
Export Addresses to CSV File
2021-04-06 08:40:43 +02:00
Craig Raw 579b9a685b automatically switch between internal and external tor proxy as required when connecting to server 2021-04-05 14:56:23 +02:00
Jimbo 57b3fd99bc Correcting address column in exported CSV, removing Derivation column. 2021-04-05 07:00:04 -04:00
Craig Raw c3ae98f3d1 add passport multisig wallet export 2021-04-05 12:19:18 +02:00
Craig Raw d635815607 reduce java-based cpu usage when scanning 2021-04-05 11:29:13 +02:00
Craig Raw e524396aaf add trezor one passphrase toggle capability 2021-04-02 15:26:43 +02:00
Craig Raw 08edc04c6d save psbts in binary format by default, add file submenu item for base64 2021-04-02 14:12:48 +02:00
Jimbo e88d6265b4 Adding export button for saving addresses as CSV 2021-04-02 06:29:15 -04:00
Craig Raw 5d91f033c0 add ssl server certificate pinning 2021-04-02 12:27:04 +02:00
Craig Raw 771bd1545c fix export of sparrow wallet from settings screen 2021-04-01 12:41:27 +02:00
Craig Raw f65fec66bb upgrade to hummingbird 1.5.5 2021-04-01 12:09:03 +02:00
Craig Raw 9c6dbeec69 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-04-01 09:52:37 +02:00
Craig Raw 563af71ed2 log errors on failed qr psbt parsing 2021-04-01 09:52:19 +02:00
craigrawandGitHub a4579c953a Merge pull request #82 from finab1/patch-1
Update README.md
2021-03-27 07:38:38 +02:00
finab1andGitHub 6a3c3de67c Update README.md
Grammer correction in README.
2021-03-27 15:40:47 +11:00
Craig Raw f8fa929166 upgrade to hummingbird 1.5.4 which handles legacy encoding in uppercase 2021-03-24 14:37:06 +02:00
Craig Raw 6aaf532051 v1.3.1 2021-03-24 13:33:41 +02:00
Craig Raw fb59bdfff4 update fee rate priority as fee prediction changes 2021-03-24 13:26:23 +02:00
Craig Raw c8aa1b0a19 increase tab icon size by 1pt 2021-03-24 13:17:49 +02:00
Craig Raw 11a201b3f5 fix webcam opening/closing issues, display progress bar for qr animation scanning progress 2021-03-24 12:59:33 +02:00
Craig Raw b74741bccb add sparrow wallet file export, dont unnecessarily ask for password when exporting 2021-03-22 14:39:58 +02:00
Craig Raw b8739ace4f remove order resources, wait for jdk16 for deterministic jlink builds across servers 2021-03-22 12:02:58 +02:00
Craig Raw cd674ee9be order resources in merged module 2021-03-22 11:31:30 +02:00
Craig Raw 8b3ed8171b fix archive issues 2021-03-20 14:48:24 +02:00
Craig Raw 28ee16279c deterministic builds on linux and windows 2021-03-20 14:13:10 +02:00
Craig Raw ff43db0842 retain messages from rpc errors when loading wallets with bwt 2021-03-20 11:49:17 +02:00
Craig Raw 8ddac9acba reduce target blocks range, predicting too far ahead leads to incorrect estimates 2021-03-19 18:34:53 +02:00
Craig Raw 0c2554da72 add export button to wallet settings 2021-03-19 14:34:29 +02:00
Craig Raw 1274d56fce ensure fees decrease as target blocks increase, show priority on target blocks slider track 2021-03-19 13:35:59 +02:00
Craig Raw 8d9e253f93 update wallet in walletform on label and keystore source changes, detect threshold change 2021-03-19 12:27:08 +02:00
Craig Raw 9166160008 always show wallet name on password dialog 2021-03-18 11:59:59 +02:00
Craig Raw 35eb8fcdf9 add suffix to rbf transaction labels to indicate transaction has been replaced 2021-03-18 11:34:50 +02:00
Craig Raw d34c8b8bde add menu command to install udev rules via hwi on linux 2021-03-17 13:53:38 +02:00
Craig Raw 80a079cbee upgrade to hwi 2.0.0-final 2021-03-17 12:58:22 +02:00
Craig Raw 5d181d1abb upgrade to bwt 0.2.3 2021-03-17 12:39:55 +02:00
Craig Raw 6fbd332b4d add preference option to load recent wallets 2021-03-17 11:59:58 +02:00
Craig Raw 211e5952aa various minor ui improvements 2021-03-17 11:39:46 +02:00
Craig Raw 4078c61d6b add tab label icons, show loading and failure status there 2021-03-16 12:07:55 +02:00
Craig Raw 8b7d1e6888 try proxy setting if internal tor cant start 2021-03-15 14:07:48 +02:00
Craig Raw 7c6daf2e11 scan and import connected devices from import wallet dialog 2021-03-15 13:03:09 +02:00
Craig Raw e1f405d886 default to hd capture on new osx installs 2021-03-15 11:25:30 +02:00
Craig Raw 1d73956853 fixes to minimum and pay then replace fee rates 2021-03-15 10:34:18 +02:00
Craig Raw 205f78151a revert to original hwi file permissions 2021-03-15 09:53:20 +02:00
Craig Raw 5853eb01f5 unpack hwi to all executable file permissions 2021-03-14 12:59:01 +02:00
Craig Raw 1a5e71f852 remove debug on logging config 2021-03-12 18:42:59 +02:00
Craig Raw 7053e7b225 fix logback conflict 2021-03-12 18:32:16 +02:00
Craig Raw 5bee26a8cc followup to jul logging bridge 2021-03-12 18:29:04 +02:00
Craig Raw b9caff352b actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:14:07 +02:00
Craig Raw 73089ce99c actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:12:42 +02:00
Craig Raw 34080ae8b3 try to quieten logs with spurious css warnings 2021-03-12 17:29:13 +02:00
Craig Raw 307c7d53f8 add error logging when enumerate fails 2021-03-12 17:19:54 +02:00
Craig Raw a6d2a6563b Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-03-12 17:15:18 +02:00
Craig Raw 577e570c99 update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 17:14:23 +02:00
Craig Raw 06411d4e2f update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 11:36:11 +02:00
Craig Raw 3e287bfb2d dont start version check service if not on mainnet 2021-03-12 10:46:59 +02:00
Craig Raw 9872fc9221 v1.3.0 2021-03-11 14:48:41 +02:00
Craig Raw 84d2db427c dont allow scheduled enumerate to run while interactive commands are running 2021-03-11 14:37:19 +02:00
Craig Raw cfbf9187e6 update linux and windows hwi binaries to post rc.2 HEAD 2021-03-11 14:01:00 +02:00
Craig Raw 5478ac05b8 show error when importing tpubs from usb on mainnet etc 2021-03-11 11:39:19 +02:00
Craig Raw 836bd6bc40 display error on sendpin correctly 2021-03-11 11:13:30 +02:00
Craig Raw 1bab07fc5f dont allow duplicate loading wallet references 2021-03-11 10:37:30 +02:00
Craig Raw 4b3de044c3 remove unused --addr_type parameter when using hwi displayaddress with --desc 2021-03-11 10:36:44 +02:00
Craig Raw f2c2fc6071 fix exception on debug logging when sending from two or more utxos on the same derivation 2021-03-11 10:33:51 +02:00
Craig Raw 4dd52eb9dd handle csv export with mempool txes 2021-03-11 10:08:41 +02:00
Craig Raw e45fdc72a0 reduce log level on electrs notification errors to quieten logs 2021-03-10 14:17:40 +02:00
Craig Raw d67dd515c4 stop progress bar when loading wallet is closed 2021-03-10 13:49:21 +02:00
Craig Raw ff962aa5f0 add overpaid indicator to fee priority 2021-03-10 13:16:08 +02:00
Craig Raw c0e44b2c55 show online docs and log file from help menu 2021-03-10 12:51:17 +02:00
Craig Raw f0c239d625 upgrade to hwi-2.0.0-rc.2, display addresses on usb devices for multisig wallets 2021-03-10 11:31:19 +02:00
Craig Raw 58a31f435e show tooltips on fee priority label, minor ui improvements 2021-03-10 11:30:06 +02:00
Craig Raw c49439d541 add menu item to show introduction again 2021-03-08 17:00:32 +02:00
Craig Raw 0070cbfb1e use and display fractional tx virtual size, indicate sizes on unfinalized txes may change 2021-03-08 16:22:22 +02:00
Craig Raw 7ed75fc83d add fee rate priority indicator 2021-03-08 15:25:03 +02:00
Craig Raw 13d701b0a7 indicate when initially loading a wallets transactions in status bar 2021-03-08 10:45:29 +02:00
Craig Raw 07ed9c857b improve placeholder text for private electrum url hostname 2021-03-08 09:36:21 +02:00
Craig Raw 32ab53ba03 append tor debug output to test results 2021-03-08 09:35:21 +02:00
Craig Raw 68344e981c set fiat value on scanning payment uri 2021-03-08 09:21:04 +02:00
Craig Raw 10c1574a4a provide framework for application logging 2021-03-08 09:18:48 +02:00
Craig Raw 3a853e2ab1 fix text width issue with overlay on linux 2021-03-05 13:18:24 +02:00
Craig Raw 0f639dc2c2 clear transport on a failed test connection, try ssl or vice-versa on electrum connection failure 2021-03-05 09:16:00 +02:00
Craig Raw a801773545 max button retains state to adjust amount as fee changes 2021-03-04 13:00:39 +02:00
Craig Raw ffbf4bb0db default to mempool size fee rates selection if not configured 2021-03-03 15:36:33 +02:00
Craig Raw 11e46155a1 indicate welcome dialog connections are demonstration only 2021-03-03 15:19:56 +02:00
Craig Raw f369b3ad00 add command line option to set log level 2021-03-01 16:32:11 +02:00
Craig Raw 6068666415 prompt to retry on invalid password 2021-03-01 15:48:47 +02:00
Craig Raw 6a58e8a799 dont lose labels when rescanning, even if app is restarted 2021-03-01 15:27:54 +02:00
Craig Raw 3bd2f69157 add change password button, delete backups option on changing password 2021-02-26 16:03:18 +02:00
Craig Raw 03ec0c9da1 v1.2.0 2021-02-25 12:08:19 +02:00
Craig Raw 47fc0e11eb test various possible derivation paths when electrum seed is provided 2021-02-25 11:54:47 +02:00
Craig Raw 27f4b3b3ae add signet support 2021-02-24 13:39:35 +02:00
Craig Raw bb8c9a19cf fix configure later path 2021-02-24 11:11:31 +02:00
Craig Raw 16da6ae224 cross-platform followup 2021-02-23 16:19:04 +02:00
Craig Raw 152b55f7f0 replace welcome dialog 2021-02-23 13:56:28 +02:00
Craig Raw 62d83151d7 add config option to disable script type derivation validation 2021-02-22 16:05:18 +02:00
Craig Raw 9060a4b284 ensure psbt files always have correct extension 2021-02-22 14:55:31 +02:00
Craig Raw 759de8e047 add public server retry period with backoff 2021-02-18 09:27:01 +02:00
Craig Raw 1e3dd61b57 followup 2021-02-17 13:57:11 +02:00
Craig Raw d13bc8c313 add additional public servers, restart immediately on another when public server connection fails 2021-02-17 13:42:19 +02:00
Craig Raw 9b6ef1f3ca change export csv icon 2021-02-17 11:18:29 +02:00
Craig Raw 173078dd8d followup to script type descriptions 2021-02-17 11:10:08 +02:00
Craig Raw ae2db6e204 improve script type descriptions and help text 2021-02-17 10:24:21 +02:00
Craig Raw 5abea8c594 followup fixes for linux 2021-02-17 10:00:26 +02:00
Craig Raw cb94d91e98 update welcome and about dialogs for public electrum server 2021-02-16 16:57:10 +02:00
Craig Raw 454e4b4c4b add public electrum server type 2021-02-16 16:38:36 +02:00
Craig Raw 48a63c25d2 adjust csv icon position 2021-02-16 12:05:57 +02:00
Craig Raw 0fdc90523e export transactions to csv 2021-02-16 11:57:34 +02:00
Craig Raw 3203308d82 maybe fix help tooltips not appearing 2021-02-16 10:08:44 +02:00
Craig Raw 83a7f5c562 v1.1.0 2021-02-11 11:41:37 +02:00
Craig Raw 55e0648fc8 fix help text to match updated cobo firmware 2021-02-11 11:16:51 +02:00
Craig Raw b517b575da add custom dmg installer background 2021-02-11 11:15:38 +02:00
Craig Raw 890b4098d7 add refresh wallet menu item 2021-02-10 10:23:42 +02:00
Craig Raw 11a0e3765b avoid losing tx, address and txo labels when performing a full wallet refresh 2021-02-09 17:32:36 +02:00
Craig Raw 3ffb09c5e6 fix java version 2021-02-08 19:58:03 +02:00
Craig Raw 622b681109 add blue wallet vault import and export and add wallet export via qr 2021-02-08 16:29:59 +02:00
Craig Raw c27a576b3d make passphrase entry a masked password field 2021-02-08 12:42:46 +02:00
Craig Raw 7bc8bdc13e provide help tooltips on policy types and script types 2021-02-08 12:31:34 +02:00
Craig Raw 0097a1ecbc also show legacy qr display for imported keystores 2021-02-08 09:36:24 +02:00
Craig Raw 525d57ec73 handle rbf fee increase for consolidation outputs 2021-02-04 12:14:42 +02:00
Craig Raw e3d7bb57ee move internal tor to a top level app service, support non-ssl proxying 2021-02-04 12:00:49 +02:00
Craig Raw 095518e858 fix bitcoin core cookie dir not present npe 2021-01-28 12:45:12 +02:00
Craig Raw ac470f64c1 revert dynamic tx detail pane 2021-01-28 12:34:32 +02:00
Craig Raw 30215b0dac dynamically resize components on window height change 2021-01-28 12:29:20 +02:00
Craig Raw 563252c823 v1.0.0 2021-01-28 11:51:38 +02:00
Craig Raw 3a7ffe95e8 fix core wallet field - disabled when connected 2021-01-28 11:41:28 +02:00
Craig Raw b588a4ee02 always show scan time remaining 2021-01-28 11:19:57 +02:00
Craig Raw ab0611fabc update bwt to latest HEAD 2021-01-28 11:02:19 +02:00
Craig Raw a121bb5f26 add passport hww single and multisig importing 2021-01-28 10:43:54 +02:00
Craig Raw 2fc01e0345 resize on initial scene change 2021-01-27 10:08:47 +02:00
Craig Raw 650c3657a5 set tx hex divider lower when window height is reduced 2021-01-27 09:16:58 +02:00
Craig Raw d836eb515e further reduce min height to 730px on all platforms 2021-01-27 08:57:46 +02:00
Craig Raw 717bc9fec2 set default core wallet name if offline mode is initially selected 2021-01-26 18:28:07 +02:00
Craig Raw 1ad6da86b1 reduce tx diagram height to fit min resolution of 768px 2021-01-26 18:25:34 +02:00
Craig Raw 54680a5692 fix bug with trezor/keepkey keystore import, show hwi errors on all related dialogs 2021-01-26 14:39:49 +02:00
Craig Raw 6ef5e79b39 greater min window height on windows and linux 2021-01-26 13:23:46 +02:00
Craig Raw 1dbf51b406 reduce min window height by 50px 2021-01-26 12:19:25 +02:00
Craig Raw 8a3aa0d06e revert to java 14.0.2 on windows due to https://bugs.openjdk.java.net/browse/JDK-8254920 2021-01-25 16:55:21 +02:00
Craig Raw ca99efa215 only open mac-specific module on mac build 2021-01-25 15:54:12 +02:00
Craig Raw cc202ce4ca minor ui tweaks 2021-01-25 15:15:10 +02:00
Craig Raw ad44fe2335 use java major version only 2021-01-25 14:44:38 +02:00
Craig Raw 452242f208 upgrade github actions jdk to 15.0.1.hs.adpt 2021-01-25 14:41:33 +02:00
Craig Raw ff83af2e0d add hd capture toggle for qr scanning 2021-01-25 14:00:17 +02:00
Craig Raw aacecc8517 initialize bwt only on connection, default to core multi-wallet use 2021-01-20 13:03:47 +02:00
Craig Raw a9a3eef157 truncate long electrum wallet labels 2021-01-18 13:39:12 +02:00
Craig Raw 7fc37c21fd v0.9.10 2021-01-14 12:35:45 +02:00
Craig Raw ff828b483b improve status bar feedback when starting bwt, syncing and scanning 2021-01-13 20:25:55 +02:00
Craig Raw 64a9cce837 log level revert and minor startup fixes 2021-01-13 16:23:02 +02:00
Craig Raw caa7ef429c add backup confirmation overlay to bip39 list generation process 2021-01-13 15:43:39 +02:00
Craig Raw 38818d8222 always set charset to utf-8 when converting from bytes to string 2021-01-13 12:17:01 +02:00
Craig Raw fd7d2232a1 only start bwt with valid wallets 2021-01-13 11:40:29 +02:00
Craig Raw b5eb59344c avoid npe on new wallet 2021-01-13 11:33:20 +02:00
Craig Raw 3093e4906c fix npe on initial startup #2 2021-01-13 11:27:55 +02:00
Craig Raw 2faddc1133 fix npe on initial startup 2021-01-13 11:23:38 +02:00
Craig Raw a2ead56593 Merge branch 'bwt' 2021-01-13 09:12:01 +02:00
Craig Raw 03adc1f9f0 fix drongo head 2021-01-13 09:10:32 +02:00
Craig Raw 7bc707bcaa update linux and windows bwt 2021-01-13 08:46:57 +02:00
Craig Raw 7ae74e3000 handle mode and startup config better 2021-01-13 08:11:10 +02:00
Craig Raw 94da3b37b6 show tx count, set default server type 2021-01-12 14:43:30 +02:00
Craig Raw c6689228d0 handle new coldcard firmware p2sh_p2wsh import name change 2021-01-12 13:50:32 +02:00
Craig Raw d847da9d61 bwt preferences and connection handling improvements 2021-01-12 13:24:11 +02:00
Craig Raw 6637ea09bf fix isconnecting with ready status flag 2021-01-12 10:37:22 +02:00
Craig Raw 99959470e2 show warning before changing a wallet with transactions 2021-01-12 10:15:42 +02:00
Craig Raw 826162ba9f various bwt related tweaks 2021-01-11 18:26:45 +02:00
Craig Raw 546ccd66b3 upgrade to bwt with earlier termination, various bitcoin core related ui changes 2021-01-11 17:31:28 +02:00
Craig Raw 628b15a3b5 support broadcasting wallet and non-wallet loaded txes 2021-01-11 12:27:28 +02:00
Craig Raw 72281233e1 Merge branch 'master' into bwt 2021-01-08 17:01:32 +02:00
Craig Raw c68722e420 Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-01-08 16:11:59 +02:00
Craig Raw 901a4520fd safe multithreading config writes 2021-01-08 16:11:36 +02:00
Craig Raw 3e083b907d bwt fixes and startup status 2021-01-08 16:10:00 +02:00
Craig Raw 3c4b25ecee bwt threading and wallet rescanning 2021-01-07 13:04:42 +02:00
craigrawandGitHub c23bbb9e02 Merge pull request #55 from bitcoinhodler/grammar
Fix grammar errors in README
2021-01-01 16:24:13 +02:00
Craig Raw 94897e9744 remove folder 2021-01-01 16:15:52 +02:00
Craig Raw d722c4972a initialise logger after configuring user defined application home 2021-01-01 10:40:08 +02:00
Bitcoin Hodler e93df8273b Fix grammar errors in README
"It's" means "it is".
2020-12-27 19:50:41 +00:00
Craig Raw a1c65cff75 initial commit of bwt integration 2020-12-20 12:34:36 +02:00
Craig Raw 18bc7bf302 improve debug logging for wallet history updates 2020-12-17 08:38:41 +02:00
Craig Raw 17bb442c78 ensure socket inputstream is buffered 2020-12-17 08:37:26 +02:00
Craig Raw 3134a9ad42 reduce time to check broadcasted tx 2020-12-16 12:37:38 +02:00
Craig Raw eda96af607 shorten details link to prevent scrollbars appearing 2020-12-16 12:10:01 +02:00
Craig Raw 038069f6e6 add scheduled service to check mempool after broadcast 2020-12-15 10:29:13 +02:00
Craig Raw ee6d8028f8 v0.9.9 2020-12-10 08:59:31 +02:00
Craig Raw 5d404f8c30 reset transaction menu shortcuts 2020-12-10 08:54:08 +02:00
Craig Raw 94662922a8 add keyboard shortcuts, fix non-mac menu exit 2020-12-10 08:38:55 +02:00
Craig Raw 582065e7f0 add specter diy keystore import 2020-12-09 17:16:44 +02:00
Craig Raw ccead92388 add view option to hide used empty addresses 2020-12-09 16:06:30 +02:00
Craig Raw 2b47740539 dont post open wallets event until scene set 2020-12-09 12:10:34 +02:00
Craig Raw 668dd88436 fix initial theme selection 2020-12-09 08:08:37 +02:00
Craig Raw 76820377ae change theme on all app windows 2020-12-08 17:24:22 +02:00
Craig Raw 6a1c3fa3da support opening wallets in new windows 2020-12-08 12:32:59 +02:00
Craig Raw dfa781e16c subscription handling for multiple app windows 2020-12-07 14:12:11 +02:00
Craig Raw 722fd84ec1 cancel running services before restart 2020-12-07 10:55:29 +02:00
Craig Raw 39d1e686f4 refactor singleton services from appcontroller to appservices 2020-12-04 15:42:17 +02:00
Craig Raw 7461b47466 move show transaction hex setting into config 2020-12-04 10:10:17 +02:00
Craig Raw 572e451279 freeze and unfreeze utxos 2020-12-01 12:45:26 +02:00
Craig Raw 88dfde3c67 import address labels from electrum wallets 2020-12-01 10:17:29 +02:00
Craig Raw 64c3db3656 reduce min height to fit 13" mbp 2020-11-29 09:21:01 +02:00
Craig Raw f0ee99b78b more efficient chart node selection 2020-11-26 15:27:25 +02:00
Craig Raw 48336bb894 add yourkit profiler credit 2020-11-26 13:42:58 +02:00
Craig Raw c804737563 upgrade to hwi 1.2.1 2020-11-26 07:56:40 +02:00
Craig Raw d395c7897e v0.9.8 2020-11-26 07:19:47 +02:00
Craig Raw 6e2e5d7a9c fix cross-platform issues 2020-11-25 11:45:26 +02:00
Craig Raw 99a39aa83f reduce unconfirmed opacity 2020-11-25 09:56:31 +02:00
Craig Raw 2ca8b91283 add increase fee functionality for rbf transactions 2020-11-24 20:34:51 +02:00
Craig Raw 94960567b5 default to mempool.space, improve mempool size tooltip display 2020-11-24 10:05:27 +02:00
Craig Raw e14349a645 only fetch external fee rates on mainnet 2020-11-23 17:34:19 +02:00
Craig Raw ee7b741a69 fetch fee rates from configurable external sources 2020-11-23 17:26:51 +02:00
Craig Raw 02c8415a7b use proxy for exchange rate if available, move to exchange rate to net package 2020-11-23 14:15:39 +02:00
Craig Raw f305e0bdc2 reduce mempool size x axis labelling 2020-11-23 12:23:45 +02:00
Craig Raw d8b7938a63 show returned errors on hwi enumerate 2020-11-19 12:54:12 +02:00
Craig Raw 44ac7f3c0e toggle fee rate selection on send, dont show zero total rate series 2020-11-19 12:33:33 +02:00
Craig Raw 2b55b5feb3 add mempool size fee selection 2020-11-19 10:46:19 +02:00
Craig Raw 43bf6ab265 get and store fee rate histogram 2020-11-18 09:54:34 +02:00
Craig Raw 80b7ee803b separate keystore and wallet scannable, improve cobo importing 2020-11-18 08:37:51 +02:00
Craig Raw 8e23bd64c7 add cobo vault support with legacy qr scanning 2020-11-17 13:32:07 +02:00
Craig Raw bf6fbebd9e add wallet to wallethistorystatusevent and filter updates 2020-11-13 12:44:27 +02:00
Craig Raw 0b9052dee9 refactor wallet field push down to entry 2020-11-13 10:12:19 +02:00
Craig Raw 6460cb88a5 v0.9.7 2020-11-12 08:40:20 +02:00
Craig Raw 90ac786707 slow down qr animation 2020-11-12 08:37:33 +02:00
Craig Raw c86006335b ensure wallet tab name and wallet name are synced on wallet open 2020-11-11 17:10:46 +02:00
Craig Raw a444e2af2f ensure wallet tab name and wallet name are synced on wallet open 2020-11-11 16:31:58 +02:00
Craig Raw bcd2c966a5 add recursive lookup for tab header 2020-11-11 14:48:10 +02:00
Craig Raw f826c2d423 upgrade to hummingbird 1.4 to fix crypto-account 2020-11-11 14:04:08 +02:00
Craig Raw 37bb1c5d97 import descriptor from qr 2020-11-11 12:26:36 +02:00
Craig Raw 68da8e6027 add support for UR registry types crypto-address, crypto-hdkey, crypto-output and crypto-account 2020-11-11 09:51:50 +02:00
Craig Raw 87fe0accd5 Use broader matching on HWI wallet types (eg bitbox02_btconly) 2020-11-05 16:46:01 +02:00
Craig Raw 8d413e839c handle server returning history with the same tx at multiple heights (electrs issue #316) 2020-11-05 16:29:15 +02:00
Craig Raw 1392199f5c add tx tree icons and input descriptions when signing 2020-11-05 10:31:54 +02:00
Craig Raw dc33b64a9c further payjoin tweaks #2 2020-11-04 16:33:05 +02:00
Craig Raw 1b220c72ea further payjoin tweaks 2020-11-04 16:32:17 +02:00
Craig Raw bcf6f77340 improve wallet import error messaging, handle p2sh-p2wpkh payjoin 2020-11-04 12:10:40 +02:00
Craig Raw 0aac8bbea7 indicate payjoin input on transaction diagram 2020-11-03 15:00:34 +02:00
Craig Raw e1acaa8a78 add payjoin (bip78) support 2020-11-03 13:54:18 +02:00
Craig Raw 13a486597c Remove illegal / characters from output descriptors 2020-11-02 07:49:55 +02:00
Craig Raw 389ce2180d accept bip21 uri in pay to field 2020-10-29 11:45:10 +02:00
Craig Raw a7a3ad012c add stacktrace on null node 2020-10-29 11:24:56 +02:00
Craig Raw 9f5eb41d90 dont add change node if no change present 2020-10-29 11:23:50 +02:00
Craig Raw 3a4d385560 avoid casting to int when comparing longs 2020-10-27 11:11:35 +02:00
Craig Raw d3881f76b9 default to trezor t on import 2020-10-27 11:09:31 +02:00
Craig Raw be5a48ce71 handle p2sh default derivation change and other small fixes 2020-10-26 15:12:10 +02:00
Craig Raw c8cd2d1cd9 v0.9.6 2020-10-21 11:22:03 +02:00
Craig Raw 5789e1c215 request password on wallet import 2020-10-21 11:11:18 +02:00
Craig Raw f48fb610e8 show no extension files in linux 2020-10-21 09:41:49 +02:00
Craig Raw 3aeba79268 use abbreviated derivation paths 2020-10-20 12:18:23 +02:00
Craig Raw 4a030e9b6e avoid ellipsis on unit dropdown 2020-10-20 11:45:58 +02:00
Craig Raw 2cf653702c handle no prior connection for fetching min relay fee rate 2020-10-20 11:36:23 +02:00
Craig Raw ac438ec023 send to multiple recipients 2020-10-20 10:56:08 +02:00
Craig Raw ee9247c066 handle support for multiple payments in a tx 2020-10-19 11:06:59 +02:00
Craig Raw 4ea5ec6c0a upgrade to patched openimag bridj with source see https://github.com/sarxos/webcam-capture/issues/723 2020-10-19 07:39:45 +02:00
Craig Raw 63a9a798f3 order available wallets to prioritise seed or usb keystores 2020-10-16 12:55:02 +02:00
Craig Raw 194f0f5c1b add file flag fallback to set testnet on startup 2020-10-16 12:37:19 +02:00
Craig Raw bda5c1a709 upgrade hwi to 1.2.0 2020-10-16 12:20:24 +02:00
Craig Raw 32327928b5 progressive read timeout 2020-10-16 12:08:27 +02:00
Craig Raw b448927a6e electrs server robustness and tuning improvements 2020-10-15 14:38:30 +02:00
Craig Raw ff578c938e break out UR implementation into hummingbird project 2020-10-15 10:38:33 +02:00
Craig Raw 998100505d change electrum call ordering and add retry handling for better behaviour on electrs 2020-10-13 12:48:32 +02:00
Craig Raw a893b37bb0 add per request delay to non-batched rpc calls 2020-10-12 18:59:50 +02:00
Craig Raw 4c58ebf138 correct handling of nodeTransactionMap empty map references 2020-10-12 11:08:24 +02:00
Craig Raw 76e148a107 add ability to import keystores and wallets with QR, add part protocol to QR scanner 2020-10-09 12:47:43 +02:00
Craig Raw 20f5e4d8db fix cobo vault keystore import 2020-10-09 09:59:26 +02:00
Craig Raw 5cd15f21ad fix icon naming for bitbox 2020-10-08 11:25:58 +02:00
Craig Raw b50cf77f3b rename xPub to xpub 2020-10-07 13:11:46 +02:00
Craig Raw 5e8148e5b9 v0.9.5 2020-10-07 12:37:10 +02:00
Craig Raw fa5ac7917b add cobo vault singlesig and multisig file import and export 2020-10-07 12:31:58 +02:00
Craig Raw eda712e269 add comment to transaction entry equals 2020-10-06 15:10:24 +02:00
Craig Raw 3c6adafc15 tweak help copy and ui 2020-10-06 14:50:25 +02:00
Craig Raw 639e145672 use tPubs instead of xPubs on labels in testnet 2020-10-06 12:39:30 +02:00
Craig Raw eb8d66bf25 add utility buttons to import and export xpubs with qr codes, show slip132 versions 2020-10-05 17:32:30 +02:00
Craig Raw 3a885b3a28 improve open wallet error dialog message 2020-10-05 12:33:05 +02:00
Craig Raw 7e93b6b198 fix byteword list as per https://github.com/BlockchainCommons/bc-bytewords/commit/9f7ec3c73c39c9638c581beb056ed03c5a3be9ff 2020-10-05 11:39:28 +02:00
Craig Raw f949bcc1cf attempt to import wallet file if open fails 2020-10-05 10:36:27 +02:00
Craig Raw 9e5937d2fe check history on all created tx script hashes when one is updated 2020-10-02 15:18:12 +02:00
Craig Raw 82a8f08440 upgrade to javafx 15, upgrade gradle plugins, ensure reproducible builds 2020-10-01 14:09:36 +02:00
Craig Raw c806959199 add doc for updating submodules 2020-10-01 12:04:41 +02:00
Craig Raw f7fb62fcea fix on windows 2020-10-01 12:00:38 +02:00
Craig Raw ea2783e51b testnet related changes and network improvements and logging 2020-10-01 09:11:02 +02:00
Craig Raw ff7a35e68b select alternative network, support testnet on hwi, other fixes 2020-09-29 15:45:22 +02:00
Craig Raw c68cda5b2f handle support for alternative (non-mainnet) networks 2020-09-28 14:32:02 +02:00
Craig Raw 2cb667a671 shorten tooltip delay, set tooltip font size equal 2020-09-26 12:43:51 +02:00
Craig Raw eb09076604 exclude utxos from transaction diagram 2020-09-26 12:30:33 +02:00
Craig Raw 32bc9f6a99 dont refresh invalid wallets 2020-09-25 17:56:57 +02:00
Craig Raw dc8a8dd857 copy non-xpub value 2020-09-25 17:48:24 +02:00
Craig Raw d5a26fdf5d followup to prev commit to handle button disable 2020-09-25 15:37:11 +02:00
Craig Raw e48e40da0d retrieve min fee rate, adjust block target rates and dont create txes with insufficient fee rate 2020-09-25 15:22:52 +02:00
Craig Raw e878c4ea78 hide validation error on clear send 2020-09-25 14:07:16 +02:00
Craig Raw ead8fa2207 clear send details on mempool tx, edit wallet output descriptor 2020-09-25 13:43:55 +02:00
Craig Raw 9a395f5dd1 v0.9.4 2020-09-21 10:00:35 +02:00
Craig Raw a99114cc6f dynamic blockchain form updating, minor tweaks 2020-09-21 09:06:01 +02:00
Craig Raw ea36c63ebb version check: test against predefined address 2020-09-20 15:09:54 +02:00
Craig Raw 77d3c848f9 add dark theme 2020-09-20 15:04:53 +02:00
Craig Raw f22312e04f add software update check 2020-09-18 15:48:05 +02:00
Craig Raw b9db4421df import transaction labels from electrum 2020-09-18 12:53:05 +02:00
Craig Raw b7aa73e99f show wallet loading progress 2020-09-18 10:50:41 +02:00
Craig Raw 4bad46c9e3 optimize electrum server queries 2020-09-17 15:10:56 +02:00
Craig Raw 084a3c76af start with simple RPC for maximum compatibility 2020-09-15 12:07:37 +02:00
Craig Raw 2e0ca1b4fa add built in tor 2020-09-14 10:51:38 +02:00
Craig Raw 4bc446724d fix keepkey hww 2020-09-10 11:40:21 +02:00
Craig Raw 0c56f5a9a1 add change password functionality 2020-09-09 16:15:33 +02:00
Craig Raw 38b27bb091 configurable wallet gap limit 2020-09-09 14:56:50 +02:00
Craig Raw b8c3bf1bea improve wallet descriptor and add output descriptor retrieval 2020-09-09 11:18:13 +02:00
Craig Raw e65f1ef3cc export specter with checksum 2020-09-08 12:42:15 +02:00
Craig Raw f25d2618b6 descriptor change multi to sortedmulti 2020-09-08 08:31:42 +02:00
Craig Raw 0adf94bf62 initial specter wallet import and export functionality 2020-09-07 17:23:12 +02:00
Craig Raw 7d7c5e5064 Support replacing existing keystore with watch only keystore 2020-09-04 13:00:40 +02:00
Craig Raw 10e4d8843d change bip39 confirmation wording 2020-09-03 20:54:46 +02:00
Craig Raw 6e10784e49 support usb message signing 2020-09-03 20:46:44 +02:00
Craig Raw 885efd7985 support bip 137 for message signing, but handle electrum approach as well when verifying 2020-09-03 19:26:54 +02:00
Craig Raw ec9534f8ae version 0.9.3 release 2020-09-03 13:41:12 +02:00
Craig Raw 6c90a7e0ba add message signing/verifying functionality 2020-09-03 13:36:43 +02:00
Craig Raw 9dd52f120f add ur toString 2020-09-03 09:38:16 +02:00
Craig Raw 4cdffec060 fix fiat amount update on transaction update 2020-09-01 13:13:38 +02:00
Craig Raw ed9bc882b7 minor ui fix, safe unsubscribe 2020-09-01 11:33:18 +02:00
Craig Raw 5054ab9564 disable sign button if signing wallet is closed 2020-09-01 09:50:44 +02:00
Craig Raw 7751f940f2 display output send type better, handle consolidation sends 2020-09-01 08:58:47 +02:00
Craig Raw 3f68ca43c2 dont show uninitialized devices (trezor t) 2020-09-01 08:04:58 +02:00
Craig Raw 68239e9472 improve device detection and display 2020-08-31 15:55:36 +02:00
Craig Raw 10edf31e13 dont open the same wallet twice 2020-08-31 11:47:06 +02:00
Craig Raw 61c3c4a3ac use minimum fee rate found for higher block targets if present 2020-08-31 11:28:03 +02:00
Craig Raw 5d6bb5e4f9 show better descriptions on outputs based on signing wallet 2020-08-31 11:22:15 +02:00
Craig Raw b808222faa deduplicate signed keystores on signature 2020-08-31 10:14:48 +02:00
Craig Raw 4d7d51f3f8 dont show status server banner (but show height in tooltip) 2020-08-30 13:47:31 +02:00
Craig Raw 546076ea48 avoid displaying any server response in error dialogs 2020-08-30 13:29:42 +02:00
Craig Raw 2bebb0925e wallet save fixes 2020-08-30 11:24:49 +02:00
Craig Raw 47855228d3 wallet load order, dont clear history on trivial changes 2020-08-30 11:15:45 +02:00
Craig Raw a6e41e3f9d fix icon on app installer 2020-08-29 20:44:50 +02:00
Craig Raw 04722204a6 set app name on X11 2020-08-29 20:06:50 +02:00
Craig Raw 452afe4731 tweak naming 2020-08-29 18:04:27 +02:00
Craig Raw af36d7b5a5 update readme for initial release 2020-08-29 17:56:09 +02:00
Craig Raw 7e225e794f fix commit 2020-08-28 19:41:32 +02:00
Craig Raw ffce18919f Merge branch 'master' of github.com:sparrowwallet/sparrow into master 2020-08-28 19:39:34 +02:00
Craig Raw ddcabf721e dont poll when signing 2020-08-28 19:38:55 +02:00
Craig Raw 7eb4b9c61d linux build fixes 2020-08-28 16:02:08 +02:00
Craig Raw 6dca03f8b4 build tweak #2 2020-08-28 15:34:35 +02:00
Craig Raw 135c1d1719 build tweak 2020-08-28 15:29:25 +02:00
Craig Raw c9a4fb4ec4 license 2020-08-28 15:20:31 +02:00
Craig Raw 06957b12e6 build ubuntu 2020-08-28 14:42:38 +02:00
Craig Raw d1fbe94d90 welcome page tweak 2020-08-28 14:41:26 +02:00
Craig Raw 1fea119173 shorten button width 2020-08-28 13:31:27 +02:00
Craig Raw 1ac264600a set linux path explicitly 2020-08-28 12:17:47 +02:00
Craig Raw 00db59c44f fix pixel stretching issue 2020-08-28 09:49:26 +02:00
Craig Raw 2c27f702bf add import 2020-08-27 17:41:17 +02:00
Craig Raw 3c2dfed96d locale and other windows related fixes 2020-08-27 17:16:20 +02:00
Craig Raw eb23cc8e61 osx only run options 2020-08-27 15:23:42 +02:00
Craig Raw fa0c74f06b set monospace font explicitly, add stage icons 2020-08-27 13:11:23 +02:00
Craig Raw ba017b2fcb windows fixes 2020-08-25 19:30:19 +02:00
Craig Raw 0a094332d2 fix folder name 2020-08-25 17:24:22 +02:00
Craig Raw 2cca6049be create installers, fix about border 2020-08-25 17:18:45 +02:00
Craig Raw 5be1f066df use matrix 2020-08-25 16:20:45 +02:00
Craig Raw d519c033ac bump version, cross platform builds 2020-08-25 16:10:56 +02:00
Craig Raw f8d15f73f6 platform specific changes 2020-08-25 15:31:04 +02:00
Craig Raw 350bddac84 java versioning 2020-08-25 12:17:33 +02:00
Craig Raw d1326cb621 update java point release 2020-08-25 09:17:28 +02:00
Craig Raw ae0e5894ca fix submodule url 2020-08-25 07:50:02 +02:00
Craig Raw 1a931c72bd checkout submodules 2020-08-24 17:46:28 +02:00
Craig Raw 92db6d9396 remove newline 2020-08-24 15:54:54 +02:00
Craig Raw 5fd59cc68a fix event name #2 2020-08-24 15:40:44 +02:00
Craig Raw ea9a310b22 fix event name 2020-08-24 15:39:31 +02:00
Craig Raw ee22d4a81d osx github action initial commit 2020-08-24 15:37:35 +02:00
Craig Raw c2fb6bcfee build, versioning and cross-platform about dlg 2020-08-24 14:52:05 +02:00
Craig Raw 8cde670efd log all errors to error log 2020-08-24 12:54:11 +02:00
Craig Raw f95cb67912 add coldcard single sig wallet import 2020-08-24 12:05:32 +02:00
Craig Raw 750f6483cb show alternative xpub values as tooltips 2020-08-24 10:12:16 +02:00
Craig Raw 53a590140d spend utxo improvements 2020-08-23 15:47:43 +02:00
Craig Raw 189ac75467 set null date on unconfirmed txes, dont let external utxos show as spendable 2020-08-22 17:20:43 +02:00
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
638 changed files with 54300 additions and 906 deletions
+42
View File
@@ -0,0 +1,42 @@
name: Package
on: workflow_dispatch
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 16.0.1
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '16.0.1'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build with Gradle
run: ./gradlew jpackage
- name: Package zip distribution
if: ${{ runner.os == 'Windows' }}
run: ./gradlew packageZipDistribution
- name: Package tar distribution
if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: Sparrow Build - ${{ runner.os }}
path: |
build/jpackage/*
!build/jpackage/Sparrow/
+3 -1
View File
@@ -4,4 +4,6 @@
build
/*.properties
out
*.log
*.log
build-*.sh
.DS_Store
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:craigraw/drongo.git
url = ../../sparrowwallet/drongo.git
+2
View File
@@ -0,0 +1,2 @@
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
+177
View File
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+95 -7
View File
@@ -1,11 +1,99 @@
# Sparrow
Bitcoin Wallet & Transaction Editor
# Sparrow Bitcoin Wallet
To clone this project, use `git clone --recursive git@github.com:craigraw/sparrow.git`
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
## Various ways to hex dump a file without spaces:
`xxd -p file | tr -d '\n'`
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
`hexdump -ve '1/1 "%.2x"'`
![Sparrow Wallet](https://sparrowwallet.com/assets/images/control-your-sends.png)
`od -t x1 -An file | tr -d '\n '`
## Building
To clone this project, use
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
`sudo apt install -y rpm fakeroot binutils`
The Sparrow binaries can be built from source using
`./gradlew jpackage`
When updating to the latest HEAD
`git pull --recurse-submodules`
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
## Running
If you prefer to run Sparrow directly from source, it can be launched from within the project directory with
`./sparrow`
Java 16 or higher must be installed.
## Configuration
Sparrow has a number of command line options, for example to change its home folder or use testnet:
```
./sparrow -h
Usage: sparrow [options]
Options:
--dir, -d
Path to Sparrow home folder
--help, -h
Show usage
--level, -l
Set log level
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
--network, -n
Network to use
Possible Values: [mainnet, testnet, regtest, signet]
```
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
`export SPARROW_NETWORK=testnet`
A final fallback which can be useful when running the Sparrow binary is to create a file called ``network-testnet`` in the Sparrow home folder (see below) to configure the testnet network.
Note that if you are connecting to an Electrum server when using testnet, that server will need to be running on testnet configuration as well.
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
Platform | Location
-------- | --------
OSX | ~/.sparrow
Linux | ~/.sparrow
Windows | %APPDATA%/Sparrow
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
## Reporting Issues
Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab above to report an issue. If possible, look in the sparrow.log file in the configuration directory for information helpful in debugging.
## License
Sparrow is licensed under the Apache 2 software licence.
## Credit
![Yourkit](https://www.yourkit.com/images/yklogo.png)
Sparrow Wallet uses the [Yourkit Java Profiler](https://www.yourkit.com/java/profiler/) to profile and improve performance.
YourKit supports open source projects with useful tools for monitoring and profiling Java and .NET applications.
+531 -25
View File
@@ -1,22 +1,36 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.8'
id 'org.kordamp.gradle.jdeps' version '0.9.0'
id 'org.beryx.jlink' version '2.17.4'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.24.0'
}
group 'com.sparrowwallet'
version '1.0-SNAPSHOT'
def sparrowVersion = '1.5.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
sourceCompatibility = 1.9
group "com.sparrowwallet"
version "${sparrowVersion}"
repositories {
mavenCentral()
maven { url 'https://oss.sonatype.org/content/groups/public' }
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
maven { url 'https://jitpack.io' }
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml' ]
version = "17"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
java {
@@ -24,14 +38,47 @@ java {
}
dependencies {
//Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo')) {
exclude group: 'org.hamcrest'
exclude group: 'junit'
}
implementation('com.google.guava:guava:28.2-jre')
implementation('com.google.code.gson:gson:2.8.6')
implementation('com.h2database:h2:2.0.202')
implementation('com.zaxxer:HikariCP:4.0.3')
implementation('org.jdbi:jdbi3-core:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
implementation('org.flywaydb:flyway-core:7.10.6-SNAPSHOT')
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') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('com.beust:jcommander:1.81')
implementation('com.github.arteam:simple-json-rpc-core:1.0')
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
}
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet:hummingbird:1.6.4')
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
}
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@@ -40,44 +87,503 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.25')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
testImplementation('junit:junit:4.12')
}
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
application {
mainModule = 'com.sparrowwallet.sparrow'
mainClass = 'com.sparrowwallet.sparrow.MainApp'
}
compileJava {
options.with {
fork = true
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"])
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/**"
}
}
}
test {
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
}
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 = ["--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=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
}
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 'jdk.crypto.cryptoki'
requires 'java.management'
requires 'io.leangen.geantyref'
uses 'org.flywaydb.core.extensibility.FlywayExtension'
uses 'org.flywaydb.core.internal.database.DatabaseType'
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
launcher {
name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--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=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson",
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
if(os.macOsX) {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "0.5"
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
installerOptions = [
'--file-associations', 'src/main/resources/associations.properties',
]
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
if (org.gradle.internal.os.OperatingSystem.current().macOsX) {
imageOptions += ['--icon', 'src/main/resources/sparrow.icns']
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
installerType = "dmg"
}
}
}
task removeGroupWritePermission(type: Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
}
task packageZipDistribution(type: Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip"
destinationDirectory = file("$buildDir/jpackage")
from("$buildDir/jpackage/") {
include "Sparrow/**"
}
}
task packageTarDistribution(type: Tar) {
dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP
from("$buildDir/jpackage/") {
include "Sparrow/**"
}
}
extraJavaModuleInfo {
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
exports('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.core.async')
exports('com.fasterxml.jackson.core.base')
exports('com.fasterxml.jackson.core.exc')
exports('com.fasterxml.jackson.core.filter')
exports('com.fasterxml.jackson.core.format')
exports('com.fasterxml.jackson.core.io')
exports('com.fasterxml.jackson.core.json')
exports('com.fasterxml.jackson.core.json.async')
exports('com.fasterxml.jackson.core.sym')
exports('com.fasterxml.jackson.core.type')
exports('com.fasterxml.jackson.core.util')
uses('com.fasterxml.jackson.core.ObjectCodec')
}
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
requires('com.fasterxml.jackson.core')
exports('com.fasterxml.jackson.annotation')
}
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
requires('java.desktop')
requires('java.logging')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('java.sql')
requires('java.xml')
exports('com.fasterxml.jackson.databind')
exports('com.fasterxml.jackson.databind.annotation')
exports('com.fasterxml.jackson.databind.cfg')
exports('com.fasterxml.jackson.databind.deser')
exports('com.fasterxml.jackson.databind.deser.impl')
exports('com.fasterxml.jackson.databind.deser.std')
exports('com.fasterxml.jackson.databind.exc')
exports('com.fasterxml.jackson.databind.ext')
exports('com.fasterxml.jackson.databind.introspect')
exports('com.fasterxml.jackson.databind.json')
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
exports('com.fasterxml.jackson.databind.jsonschema')
exports('com.fasterxml.jackson.databind.jsontype')
exports('com.fasterxml.jackson.databind.jsontype.impl')
exports('com.fasterxml.jackson.databind.module')
exports('com.fasterxml.jackson.databind.node')
exports('com.fasterxml.jackson.databind.ser')
exports('com.fasterxml.jackson.databind.ser.impl')
exports('com.fasterxml.jackson.databind.ser.std')
exports('com.fasterxml.jackson.databind.type')
exports('com.fasterxml.jackson.databind.util')
uses('com.fasterxml.jackson.databind.Module')
}
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
exports('tornadofx.control')
requires('javafx.controls')
}
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
exports('com.github.arteam.simplejsonrpc.core.annotation')
exports('com.github.arteam.simplejsonrpc.core.domain')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.databind')
requires('org.jetbrains.annotations')
}
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
exports('com.github.arteam.simplejsonrpc.client')
exports('com.github.arteam.simplejsonrpc.client.builder')
exports('com.github.arteam.simplejsonrpc.client.exception')
requires('com.fasterxml.jackson.core')
requires('com.fasterxml.jackson.databind')
requires('simple.json.rpc.core')
}
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
exports('com.github.arteam.simplejsonrpc.server')
requires('simple.json.rpc.core')
requires('com.google.common')
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
requires('java.desktop')
requires('com.nativelibs4java.bridj')
requires('org.slf4j')
}
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
}
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
exports('com.csvreader')
}
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
exports('org.slf4j.bridge')
requires('java.logging')
requires('org.slf4j')
}
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
exports('org.zeromq')
}
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
exports('org.json.simple')
}
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
exports('ch.qos.logback.classic')
requires('org.slf4j')
requires('logback.core')
requires('java.xml')
requires('java.logging')
}
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
exports('mu')
requires('kotlin.stdlib')
requires('org.slf4j')
}
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
exports('com.google.common.util.concurrent.internal')
}
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
exports('com.google.common.eventbus')
exports('com.google.common.net')
exports('com.google.common.base')
exports('com.google.common.collect')
exports('com.google.common.io')
exports('com.google.common.primitives')
exports('com.google.common.math')
requires('failureaccess')
requires('java.logging')
}
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
}
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
exports('io.leangen.geantyref')
}
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event')
exports('org.fxmisc.richtext.model')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx')
requires('org.fxmisc.wellbehaved')
}
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
exports('org.fxmisc.flowless')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
requires('org.reactfx.reactfx')
}
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
exports('org.reactfx')
exports('org.reactfx.value')
exports('org.reactfx.collection')
exports('org.reactfx.util')
requires('javafx.base')
requires('javafx.graphics')
requires('javafx.controls')
}
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
exports('io.reactivex.rxjavafx.schedulers')
requires('io.reactivex.rxjava2')
requires('javafx.graphics')
}
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
requires('javafx.base')
requires('javafx.graphics')
}
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
}
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
exports('com.sparrowwallet.hummingbird.registry')
requires('co.nstant.in.cbor')
}
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.25.jar', 'com.sparrowwallet.nightjar', '0.2.25') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
requires('org.bouncycastle.provider')
requires('com.fasterxml.jackson.databind')
requires('com.fasterxml.jackson.annotation')
requires('com.fasterxml.jackson.core')
requires('logback.classic')
requires('org.json')
requires('io.reactivex.rxjava2')
exports('com.samourai.http.client')
exports('com.samourai.tor.client')
exports('com.samourai.wallet.api.backend')
exports('com.samourai.wallet.api.backend.beans')
exports('com.samourai.wallet.client.indexHandler')
exports('com.samourai.wallet.hd')
exports('com.samourai.wallet.util')
exports('com.samourai.wallet.bip47.rpc')
exports('com.samourai.wallet.bip47.rpc.java')
exports('com.samourai.wallet.cahoots')
exports('com.samourai.wallet.cahoots.psbt')
exports('com.samourai.wallet.cahoots.stonewallx2')
exports('com.samourai.soroban.cahoots')
exports('com.samourai.soroban.client')
exports('com.samourai.soroban.client.cahoots')
exports('com.samourai.soroban.client.meeting')
exports('com.samourai.soroban.client.rpc')
exports('com.samourai.wallet.send')
exports('com.samourai.whirlpool.client.event')
exports('com.samourai.whirlpool.client.wallet')
exports('com.samourai.whirlpool.client.wallet.beans')
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
exports('com.samourai.whirlpool.client.whirlpool')
exports('com.samourai.whirlpool.client.whirlpool.beans')
exports('com.samourai.whirlpool.client.wallet.data.pool')
exports('com.samourai.whirlpool.client.wallet.data.utxo')
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
exports('com.samourai.whirlpool.client.wallet.data.supplier')
exports('com.samourai.whirlpool.client.mix.handler')
exports('com.samourai.whirlpool.client.mix.listener')
exports('com.samourai.whirlpool.protocol.beans')
exports('com.samourai.whirlpool.protocol.rest')
exports('com.samourai.whirlpool.client.tx0')
exports('com.samourai.wallet.segwit.bech32')
exports('com.samourai.whirlpool.client.wallet.data.wallet')
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
exports('com.samourai.whirlpool.client.wallet.data.walletState')
exports('com.sparrowwallet.nightjar.http')
exports('com.sparrowwallet.nightjar.stomp')
exports('com.sparrowwallet.nightjar.tor')
}
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
exports('com.zeroleak.throwingsupplier')
}
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
exports('com.squareup.okhttp')
}
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
exports('com.auth0.jwt')
}
module('json-20180130.jar', 'org.json', '1.0') {
exports('org.json')
}
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto')
}
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
exports('com.google.protobuf')
}
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
exports('org.apache.commons.text')
}
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
requires('kotlin.stdlib')
requires('commons.compress')
requires('org.tukaani.xz')
requires('java.management')
requires('io.github.microutils.kotlin.logging')
}
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
exports('net.freehaven.tor.control')
}
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
exports('org.apache.commons.compress')
requires('org.tukaani.xz')
}
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
exports('org.tukaani.xz')
}
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
exports('com.runjva.sourceforge.jsocks.protocol')
requires('org.slf4j')
}
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'org.ow2.asm:asm:8.0.1'
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
// here we register our plugin with an ID
register("extra-java-module-info") {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
}
}
@@ -0,0 +1,54 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.plugins.JavaPlugin;
/**
* Entry point of our plugin that should be applied in the root project.
*/
public class ExtraModuleInfoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
// setup the transform for all projects in the build
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
}
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
c -> c.getAttributes().attribute(javaModule, true));
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
t.parameters(p -> {
p.setModuleInfo(extension.getModuleInfo());
p.setAutomaticModules(extension.getAutomaticModules());
});
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
});
}
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
if (!configuration.isCanBeResolved()) {
return false;
}
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
}
}
@@ -0,0 +1,52 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.Action;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A data class to collect all the module information we want to add.
* Here the class is used as extension that can be configured in the build script
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
*/
public class ExtraModuleInfoPluginExtension {
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
private final Map<String, String> automaticModules = new HashMap<>();
/**
* Add full module information for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion) {
module(jarName, moduleName, moduleVersion, null);
}
/**
* Add full module information, including exported packages and dependencies, for a given Jar file.
*/
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
if (conf != null) {
conf.execute(moduleInfo);
}
this.moduleInfo.put(jarName, moduleInfo);
}
/**
* Add only an automatic module name to a given jar file.
*/
public void automaticModule(String jarName, String moduleName) {
automaticModules.put(jarName, moduleName);
}
protected Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
protected Map<String, String> getAutomaticModules() {
return automaticModules;
}
}
@@ -0,0 +1,174 @@
package org.gradle.sample.transform.javamodules;
import org.gradle.api.artifacts.transform.InputArtifact;
import org.gradle.api.artifacts.transform.TransformAction;
import org.gradle.api.artifacts.transform.TransformOutputs;
import org.gradle.api.artifacts.transform.TransformParameters;
import org.gradle.api.file.FileSystemLocation;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ModuleVisitor;
import org.objectweb.asm.Opcodes;
import java.io.*;
import java.util.Collections;
import java.util.Map;
import java.util.jar.*;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
/**
* An artifact transform that applies additional information to Jars without module information.
* The transformation fails the build if a Jar does not contain information and no extra information
* was defined for it. This way we make sure that all Jars are turned into modules.
*/
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
public static class Parameter implements TransformParameters, Serializable {
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
private Map<String, String> automaticModules = Collections.emptyMap();
@Input
public Map<String, ModuleInfo> getModuleInfo() {
return moduleInfo;
}
@Input
public Map<String, String> getAutomaticModules() {
return automaticModules;
}
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
this.moduleInfo = moduleInfo;
}
public void setAutomaticModules(Map<String, String> automaticModules) {
this.automaticModules = automaticModules;
}
}
@InputArtifact
protected abstract Provider<FileSystemLocation> getInputArtifact();
@Override
public void transform(TransformOutputs outputs) {
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
Map<String, String> automaticModules = getParameters().automaticModules;
File originalJar = getInputArtifact().get().getAsFile();
String originalJarName = originalJar.getName();
//Recreate jackson jars as open, non-synthetic modules
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
outputs.file(originalJar);
} else if (moduleInfo.containsKey(originalJarName)) {
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
} else if (isAutoModule(originalJar)) {
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
}
private boolean isModule(File jar) {
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
ZipEntry next = inputStream.getNextEntry();
while (next != null) {
if ("module-info.class".equals(next.getName())) {
return true;
}
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
return true;
}
next = inputStream.getNextEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
Manifest manifest = jarStream.getManifest();
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
}
private boolean isAutoModule(File jar) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private File getModuleJar(TransformOutputs outputs, File originalJar) {
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
}
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
copyEntries(inputStream, outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
Manifest manifest = inputStream.getManifest();
if(manifest == null) {
manifest = new Manifest();
}
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
copyEntries(inputStream, outputStream);
outputStream.putNextEntry(new JarEntry("module-info.class"));
outputStream.write(addModuleInfo(moduleInfo));
outputStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();
}
jarEntry = inputStream.getNextJarEntry();
}
}
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
for (String packageName : moduleInfo.getExports()) {
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
}
moduleVisitor.visitRequire("java.base", 0, null);
for (String requireName : moduleInfo.getRequires()) {
moduleVisitor.visitRequire(requireName, 0, null);
}
for (String requireName : moduleInfo.getRequiresTransitive()) {
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
}
for (String usesName : moduleInfo.getUses()) {
moduleVisitor.visitUse(usesName.replace('.', '/'));
}
moduleVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}
}
@@ -0,0 +1,62 @@
package org.gradle.sample.transform.javamodules;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
*/
public class ModuleInfo implements Serializable {
private String moduleName;
private String moduleVersion;
private List<String> exports = new ArrayList<>();
private List<String> requires = new ArrayList<>();
private List<String> requiresTransitive = new ArrayList<>();
private List<String> uses = new ArrayList<>();
ModuleInfo(String moduleName, String moduleVersion) {
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
}
public void exports(String exports) {
this.exports.add(exports);
}
public void requires(String requires) {
this.requires.add(requires);
}
public void requiresTransitive(String requiresTransitive) {
this.requiresTransitive.add(requiresTransitive);
}
public void uses(String uses) {
this.uses.add(uses);
}
public String getModuleName() {
return moduleName;
}
protected String getModuleVersion() {
return moduleVersion;
}
protected List<String> getExports() {
return exports;
}
protected List<String> getRequires() {
return requires;
}
protected List<String> getRequiresTransitive() {
return requiresTransitive;
}
protected List<String> getUses() {
return uses;
}
}
+11
View File
@@ -0,0 +1,11 @@
## Sparrow Wallet Repository Docs
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
The documentation here is mainly developer-related resources.
### [Reproducible builds](reproducible.md)
Documentation to create and verify a build of the project against the released binaries.
+78
View File
@@ -0,0 +1,78 @@
## Reproducible builds
Reproducibility is a goal of the Sparrow Wallet project.
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
Work on resolving both of these issues is ongoing.
### Reproducing a release
#### Install Java
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
```shell
sudo apt-get install -y wget apt-transport-https gnupg
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
rm adoptopenjdk-keyring.gpg
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
sudo apt update -y
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
```
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
See the installation [instructions here](https://sdkman.io/install).
Once installed, run
```shell
sdk install java 16.0.1.hs-adpt
```
#### Other requirements
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
```shell
sudo apt install -y rpm fakeroot binutils
```
#### Building the binaries
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.2"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
Thereafter, building should be straightforward:
```shell
cd sparrow
./gradlew jpackage
```
The binaries (and installers) will be placed in the `build/jpackage` folder.
#### Verifying the binaries are identical
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
Then compare all of the folders and files recursively:
```shell
diff -r build/jpackage/Sparrow /tmp/Sparrow
```
This command should have no output indicating that the two folders (and all their contents) are identical.
+1 -1
Submodule drongo updated: 0cebee3d22...c1f6a1245e
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+4 -18
View File
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env sh
cd `dirname $0`
args="$*"
args="${args%"${args##*[![:space:]]}"}"
if [ -n "$args" ]
then
./gradlew run --args="$args"
else
./gradlew run
fi
+8
View File
@@ -0,0 +1,8 @@
@echo off
set ARGS=%*
if "%ARGS%" == "" (
gradlew.bat run
) else (
gradlew.bat run --args="%ARGS%"
)
+2
View File
@@ -0,0 +1,2 @@
mime-type=x-scheme-handler/aopp
description=Verify Address Ownership URI
+2
View File
@@ -0,0 +1,2 @@
mime-type=x-scheme-handler/bitcoin
description=Bitcoin Scheme URI
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Sparrow
Comment=Sparrow
Exec=/opt/sparrow/bin/Sparrow %U
Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Unknown
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+122
View File
@@ -0,0 +1,122 @@
<?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>1.5.5</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) 2021</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.sparrowwallet.sparrow.bitcoin</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bitcoin</string>
</array>
</dict>
<dict>
<key>CFBundleURLName</key>
<string>com.sparrowwallet.sparrow.aopp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>aopp</string>
</array>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>org.bitcoin.psbt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>psbt</string>
</array>
</dict>
<key>UTTypeDescription</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>org.bitcoin.txn</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>txn</string>
</array>
</dict>
<key>UTTypeDescription</key>
<string>Bitcoin Transaction</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeIconFile</key>
<string>sparrow.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>psbt</string>
</array>
<key>CFBundleTypeName</key>
<string>Partially Signed Bitcoin Transaction</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleTypeIconFile</key>
<string>sparrow.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>txn</string>
</array>
<key>CFBundleTypeName</key>
<string>Bitcoin Transaction</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>
</dict>
</plist>
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+163
View File
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<?ifdef JpIsSystemWide ?>
<?define JpInstallScope="perMachine"?>
<?else?>
<?define JpInstallScope="perUser"?>
<?endif?>
<?define JpProductLanguage=1033 ?>
<?define JpInstallerVersion=200 ?>
<?define JpCompressedMsi=yes ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
<Product
Id="$(var.JpProductCode)"
Name="$(var.JpAppName)"
Language="$(var.JpProductLanguage)"
Version="$(var.JpAppVersion)"
Manufacturer="$(var.JpAppVendor)"
UpgradeCode="$(var.JpProductUpgradeCode)">
<Package
Description="$(var.JpAppDescription)"
Manufacturer="$(var.JpAppVendor)"
InstallerVersion="$(var.JpInstallerVersion)"
Compressed="$(var.JpCompressedMsi)"
InstallScope="$(var.JpInstallScope)" Platform="x64"
/>
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
<?ifdef JpAllowDowngrades ?>
<?ifdef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="yes"
Disallow="no"
/>
<?endif?>
<?endif?>
<?ifdef JpAllowDowngrades ?>
<?ifndef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="yes"
Disallow="yes"
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
/>
<?endif?>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<?ifdef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="no"
Disallow="no"
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
/>
<?endif?>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<?ifndef JpAllowUpgrades ?>
<MajorUpgrade
AllowDowngrades="no"
Disallow="yes"
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
/>
<?endif?>
<?endif?>
<!-- Standard required root -->
<Directory Id="TARGETDIR" Name="SourceDir"/>
<DirectoryRef Id="TARGETDIR">
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
</Component>
</DirectoryRef>
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
<ComponentGroupRef Id="Shortcuts"/>
<ComponentGroupRef Id="Files"/>
<ComponentGroupRef Id="FileAssociations"/>
<ComponentRef Id="RegistryEntries"/>
</Feature>
<?ifdef JpInstallDirChooser ?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
<?endif?>
<UI>
<?ifdef JpInstallDirChooser ?>
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
</Control>
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
</Control>
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
<Text>!(loc.message.install.dir.exist)</Text>
</Control>
</Dialog>
<!--
Run WixUI_InstallDir dialog in the default install directory.
-->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
<UIRef Id="WixUI_InstallDir" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
<?ifndef JpLicenseRtf ?>
<!--
No license file provided.
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
to exclude license dialog.
-->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
<?endif?>
<?else?>
<?ifdef JpLicenseRtf ?>
<UIRef Id="WixUI_Minimal" />
<?endif?>
<?endif?>
</UI>
<?ifdef JpLicenseRtf ?>
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
<?endif?>
</Product>
</Wix>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+3
View File
@@ -0,0 +1,3 @@
mime-type=application/psbt
extension=psbt
description=Partially Signed Bitcoin Transaction
+3
View File
@@ -0,0 +1,3 @@
mime-type=application/bitcoin-transaction
extension=txn
description=Bitcoin Transaction
@@ -0,0 +1,29 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class AboutController {
private Stage stage;
@FXML
private Label title;
public void initializeView() {
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
}
public void setStage(Stage stage) {
this.stage = stage;
}
public void close(ActionEvent event) {
stage.close();
}
public void openDonate(ActionEvent event) {
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/donate");
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.Parameter;
import com.sparrowwallet.drongo.Network;
import org.slf4j.event.Level;
public class Args {
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
public String dir;
@Parameter(names = { "--network", "-n" }, description = "Network to use")
public Network network;
@Parameter(names = { "--level", "-l" }, description = "Set log level")
public Level level;
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
public boolean help;
}
@@ -0,0 +1,83 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.DescriptorArea;
import com.sparrowwallet.sparrow.control.ScriptArea;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
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();
}
protected void initializeDescriptorField(DescriptorArea descriptorArea) {
Popup popup = new Popup();
Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg);
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeKeystore(hoverKeystore));
popup.show(descriptorArea, pos.getX(), pos.getY() + 10);
}
});
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide();
});
}
protected String describeKeystore(Keystore keystore) {
if(keystore.isValid()) {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append("/");
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
builder.append("]");
builder.append(keystore.getExtendedPublicKey().toString());
return builder.toString();
}
return "Invalid";
}
}
@@ -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,35 +1,230 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.instance.InstanceException;
import com.sparrowwallet.sparrow.instance.InstanceList;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.5.5";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
private Stage mainStage;
private static SparrowInstance sparrowInstance;
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
} else {
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
}
});
super.init();
}
@Override
public void start(Stage stage) throws Exception {
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
Parent root = transactionLoader.load();
AppController appController = transactionLoader.getController();
this.mainStage = stage;
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm());
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
stage.setTitle("Sparrow");
stage.setMinWidth(650);
stage.setMinHeight(700);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/sparrow.png")));
AppServices.initialize(this);
appController.initializeView();
boolean createNewWallet = false;
Mode mode = Config.get().getMode();
if(mode == null) {
WelcomeDialog welcomeDialog = new WelcomeDialog();
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
stage.show();
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
} else if(Network.get() == Network.MAINNET) {
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
}
}
}
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
Config.get().setHdCapture(Boolean.TRUE);
}
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
mainStage.setWidth(Config.get().getAppWidth());
mainStage.setHeight(Config.get().getAppHeight());
}
AppController appController = AppServices.newAppWindow(stage);
if(createNewWallet) {
appController.newWallet(null);
}
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
appController.openWalletFile(walletFile, false);
}
}
}
AppServices.openFileUriArguments(stage);
AppServices.get().start();
}
public static void main(String[] args) {
launch(args);
@Override
public void stop() throws Exception {
AppServices.get().stop();
Config.get().setAppWidth(mainStage.getWidth());
Config.get().setAppHeight(mainStage.getHeight());
mainStage.close();
if(sparrowInstance != null) {
sparrowInstance.freeLock();
}
}
public static void main(String[] argv) {
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase()).acceptUnknownOptions(true).build();
jCommander.parse(argv);
if(args.help) {
jCommander.usage();
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
}
if(args.dir != null) {
System.setProperty(APP_HOME_PROPERTY, args.dir);
getLogger().info("Using configured Sparrow home folder of " + args.dir);
}
if(args.network != null) {
Network.set(args.network);
} else {
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
if(envNetwork != null) {
try {
Network.set(Network.valueOf(envNetwork.toUpperCase()));
} catch(Exception e) {
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
}
}
}
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
if(testnetFlag.exists()) {
Network.set(Network.TESTNET);
}
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
if(signetFlag.exists()) {
Network.set(Network.SIGNET);
}
if(Network.get() != Network.MAINNET) {
getLogger().info("Using " + Network.get() + " configuration");
}
List<String> fileUriArguments = jCommander.getUnknownOptions();
try {
sparrowInstance = new SparrowInstance(fileUriArguments);
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
} catch(InstanceException e) {
getLogger().error("Could not access application lock", e);
}
if(!fileUriArguments.isEmpty()) {
AppServices.parseFileUriArguments(fileUriArguments);
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
}
private static Logger getLogger() {
return LoggerFactory.getLogger(MainApp.class);
}
private static class SparrowInstance extends InstanceList {
private final List<String> fileUriArguments;
public SparrowInstance(List<String> fileUriArguments) {
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
this.fileUriArguments = fileUriArguments;
}
@Override
protected void receiveMessageList(List<String> messageList) {
if(messageList != null && !messageList.isEmpty()) {
AppServices.parseFileUriArguments(messageList);
AppServices.openFileUriArguments(null);
}
}
@Override
protected List<String> sendMessageList() {
return fileUriArguments;
}
@Override
protected void beforeExit() {
getLogger().info("Opening files/URIs in already running instance, exiting...");
}
}
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow;
import javafx.application.Preloader;
import javafx.stage.Stage;
public class MainAppPreloader extends Preloader {
@Override
public void start(Stage stage) {
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Mode {
OFFLINE, ONLINE
}
@@ -1,11 +1,7 @@
package com.sparrowwallet.sparrow;
import java.io.File;
public class TabData {
private TabType type;
private File file;
private String text;
private final TabType type;
public TabData(TabType type) {
this.type = type;
@@ -15,23 +11,7 @@ public class TabData {
return type;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public enum TabType {
TRANSACTION
WALLET, TRANSACTION
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Theme {
LIGHT, DARK
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,34 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import java.io.File;
public class TransactionTabData extends TabData {
private final File file;
private final TransactionData transactionData;
public TransactionTabData(TabType type, File file, TransactionData transactionData) {
super(type);
this.file = file;
this.transactionData = transactionData;
}
public File getFile() {
return file;
}
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,137 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.controlsfx.control.StatusBar;
public class WelcomeController {
@FXML
private VBox welcomeBox;
@FXML
private VBox step1;
@FXML
private VBox step2;
@FXML
private VBox step3;
@FXML
private VBox step4;
@FXML
private StatusBar serverStatus;
@FXML
private UnlabeledToggleSwitch serverToggle;
public void initializeView() {
step1.managedProperty().bind(step1.visibleProperty());
step2.managedProperty().bind(step2.visibleProperty());
step3.managedProperty().bind(step3.visibleProperty());
step4.managedProperty().bind(step4.visibleProperty());
step2.setVisible(false);
step3.setVisible(false);
step4.setVisible(false);
welcomeBox.getStyleClass().add("offline");
serverStatus.setText("Offline");
serverToggle.addEventFilter(MouseEvent.MOUSE_RELEASED, Event::consume);
Tooltip tooltip = new Tooltip("Demonstration only - you are not connected!");
tooltip.setShowDelay(Duration.ZERO);
serverToggle.setTooltip(tooltip);
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
});
}
public boolean next() {
if(step1.isVisible()) {
step1.setVisible(false);
step2.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
});
wait.play();
return true;
}
if(step2.isVisible()) {
step2.setVisible(false);
step3.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
if(step3.isVisible()) {
step3.setVisible(false);
step4.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("private-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
}
return false;
}
public boolean back() {
if(step2.isVisible()) {
step2.setVisible(false);
step1.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("offline");
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(false);
serverStatus.setText("Offline");
});
wait.play();
return false;
}
if(step3.isVisible()) {
step3.setVisible(false);
step2.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
return true;
}
if(step4.isVisible()) {
step4.setVisible(false);
step3.setVisible(true);
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
return false;
}
}
@@ -0,0 +1,71 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.io.IOException;
public class WelcomeDialog extends Dialog<Mode> {
public WelcomeDialog() {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
try {
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
dialogPane.setContent(welcomeLoader.load());
WelcomeController welcomeController = welcomeLoader.getController();
welcomeController.initializeView();
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
AppServices.moveToActiveWindowScreen(this);
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
Button onlineButton = (Button)dialogPane.lookupButton(onlineButtonType);
Button offlineButton = (Button)dialogPane.lookupButton(offlineButtonType);
nextButton.managedProperty().bind(nextButton.visibleProperty());
backButton.managedProperty().bind(backButton.visibleProperty());
onlineButton.managedProperty().bind(onlineButton.visibleProperty());
offlineButton.managedProperty().bind(offlineButton.visibleProperty());
backButton.setDisable(true);
onlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
offlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
if(!welcomeController.next()) {
nextButton.setVisible(false);
onlineButton.setDefaultButton(true);
}
backButton.setDisable(false);
event.consume();
});
backButton.addEventFilter(ActionEvent.ACTION, event -> {
nextButton.setVisible(true);
if(!welcomeController.back()) {
backButton.setDisable(true);
}
event.consume();
});
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,111 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
private final ComboBox<StandardAccount> standardAccountCombo;
private boolean discoverAccounts = false;
public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
setTitle("Add Account");
dialogPane.setHeaderText("Choose an account to add:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
key.setFontSize(50);
dialogPane.setGraphic(key);
final VBox content = new VBox(10);
content.setPrefHeight(50);
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
List<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
}
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
}
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
discoverButton.setOnAction(event -> {
discoverAccounts = true;
});
}
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() {
@Override
public String toString(StandardAccount account) {
if(account == null) {
return "None Available";
}
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
return "Whirlpool Accounts";
}
return account.getName();
}
@Override
public StandardAccount fromString(String string) {
return null;
}
});
if(standardAccountCombo.getItems().isEmpty()) {
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
okButton.setDisable(true);
} else {
standardAccountCombo.getSelectionModel().select(0);
}
content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null));
}
public boolean isDiscoverAccounts() {
return discoverAccounts;
}
}
@@ -0,0 +1,60 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import org.controlsfx.glyphfont.Glyph;
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
public AddressCell() {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.RIGHT);
getStyleClass().add("address-cell");
}
@Override
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
super.updateItem(addressStatus, empty);
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
EntryCell.applyRowStyles(this, utxoEntry);
if (empty) {
setText(null);
setGraphic(null);
} else {
if(utxoEntry != null) {
Address address = addressStatus.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
setTooltip(tooltip);
if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
}
}
}
}
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().toString() + (duplicate ? " (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,37 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import javafx.util.StringConverter;
public class AddressStringConverter extends StringConverter<Address> {
@Override
public Address fromString(String value) {
// If the specified value is null or zero-length, return null
if(value == null) {
return null;
}
value = value.trim();
if (value.length() < 1) {
return null;
}
try {
return Address.fromString(value);
} catch(InvalidAddressException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public String toString(Address value) {
// If the specified value is null, return a zero-length String
if(value == null) {
return "";
}
return value.toString();
}
}
@@ -0,0 +1,130 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
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 && !rootEntry.getWallet().isWhirlpoolChildWallet()) {
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), 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) {
OptionalInt tableIndex = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().getIndex() == highestUsedIndex + 1).mapToInt(childEntry -> rootEntry.getChildren().indexOf(childEntry)).findFirst();
if(tableIndex.isPresent() && tableIndex.getAsInt() > 5) {
scrollTo(tableIndex.getAsInt());
}
}
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
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)));
}
}
}
}
});
}
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
});
}
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 child nodes - never remove in order to keep a full sequence
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
for(WalletNode updatedNode : updatedNodes) {
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
if(optEntry.isPresent()) {
NodeEntry existingEntry = (NodeEntry)optEntry.get();
existingEntry.refreshChildren();
} else {
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
rootEntry.getChildren().add(nodeEntry);
}
}
refresh();
}
public void updateLabel(Entry entry) {
Entry rootEntry = getRoot().getValue();
rootEntry.updateLabel(entry);
}
}
@@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.util.Duration;
public class AnimationUtil {
public static Timeline getSlowFadeOut(Node node, Duration duration, double fromValue, int numIncrements) {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double percent = ((double)numIncrements - i - 1) / numIncrements;
double opacity = percent * fromValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public static Timeline getPulse(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline pulseTimeline = getFade(node, duration, fromValue, toValue, numIncrements);
pulseTimeline.setCycleCount(Animation.INDEFINITE);
pulseTimeline.setAutoReverse(true);
return pulseTimeline;
}
public static Timeline getFade(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
Timeline fadeTimeline = new Timeline();
Duration incrementDuration = duration.divide(numIncrements);
for(int i = 0; i < numIncrements; i++) {
double percent = ((double) numIncrements - i - 1) / numIncrements; //From 99% to 0%
double opacity = (percent * (fromValue - toValue)) + toValue;
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
}
return fadeTimeline;
}
public record AnimatedNode (Node node, Timeline timeline) {}
}
@@ -0,0 +1,103 @@
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)
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
.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);
if(transactionEntry.getBlockTransaction().getDate() != null && data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
symbol.getStyleClass().add("selected");
selectedEntry = transactionEntry;
}
}
}
}
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,60 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.SendController;
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 BlockTargetFeeRatesChart extends LineChart<String, Number> {
private XYChart.Series<String, Number> feeRateSeries;
private Integer selectedTargetBlocks;
public BlockTargetFeeRatesChart(@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();
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
}
}
if(selectedTargetBlocks != null) {
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,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,98 @@
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 org.controlsfx.tools.Platform;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
class CoinCell extends TreeTableCell<Entry, Number> {
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final Tooltip tooltip;
public CoinCell() {
super();
tooltip = new Tooltip();
getStyleClass().add("coin-cell");
if(Platform.getCurrent() == Platform.OSX) {
getStyleClass().add("number-field");
}
}
@Override
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());
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
if(unit.equals(BitcoinUnit.BTC)) {
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
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);
}
if(amount.longValue() < 0) {
getStyleClass().add("negative-amount");
}
} 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 satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
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,98 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public CoinTextFormatter() {
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,111 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.StackPane;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit;
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();
}
}
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
if(getRoot() != null) {
Entry entry = getRoot().getValue();
if(entry != null && event.getWallet() != null && entry.getWallet() == event.getWallet()) {
Platform.runLater(() -> {
if(event.getErrorMessage() != null) {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
} else {
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
}
});
}
}
}
protected Node getDefaultPlaceholder(Wallet wallet) {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.setBirthDate(optDate.get());
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
}
});
if(wallet.getBirthDate() == null) {
hyperlink.setText("Scan for previous transactions?");
} else {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?");
}
stackPane.getChildren().add(hyperlink);
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
}
@@ -0,0 +1,71 @@
package com.sparrowwallet.sparrow.control;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.textfield.CustomTextField;
public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
private boolean initialized;
private boolean comboShowing;
public ComboBoxTextField() {
super();
getStyleClass().add("combo-text-field");
setupComboButtonField(super.rightProperty());
disabledProperty().addListener((observable, oldValue, newValue) -> {
if(comboProperty.isNotNull().get()) {
comboProperty.get().setVisible(!newValue);
}
});
}
private void setupComboButtonField(ObjectProperty<Node> rightProperty) {
Region showComboButton = new Region();
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
StackPane showComboButtonPane = new StackPane(showComboButton);
showComboButtonPane.getStyleClass().addAll("combo-button"); //$NON-NLS-1$
showComboButtonPane.setCursor(Cursor.DEFAULT);
showComboButtonPane.setOnMouseReleased(e -> {
if(comboProperty.isNotNull().get()) {
if(comboShowing) {
comboProperty.get().hide();
} else {
comboProperty.get().show();
}
comboShowing = !comboShowing;
if(!initialized) {
comboProperty.get().valueProperty().addListener((observable, oldValue, newValue) -> {
comboShowing = false;
Platform.runLater(() -> comboProperty.get().getSelectionModel().clearSelection());
});
initialized = true;
}
}
});
rightProperty.set(showComboButtonPane);
}
public ComboBox<?> getComboProperty() {
return comboProperty.get();
}
public ObjectProperty<ComboBox<?>> walletComboProperty() {
return comboProperty;
}
public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(comboProperty);
}
}
@@ -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);
Timeline groupFadeOut = AnimationUtil.getSlowFadeOut(confirmationGroup, Duration.minutes(10), 1.0, 10);
sequence.getChildren().add(groupFadeOut);
confirmationsProperty().unbind();
}
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,100 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.FadeTransition;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.MouseEvent;
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);
private final ChangeListener<String> selectionListener = (textObservable, textOldValue, textNewValue) -> {
if(!textNewValue.isEmpty()) {
deselect();
}
};
private final EventHandler<MouseEvent> copyHandler = event -> {
ClipboardContent content = new ClipboardContent();
content.putString(getCopyText());
Clipboard.getSystemClipboard().setContent(content);
Tooltip tooltip = new Tooltip("Copied!");
tooltip.show(this, event.getScreenX(), event.getScreenY());
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide()));
timeline.play();
};
public CopyableTextField() {
super();
getStyleClass().add("copyable-text-field");
setupCopyButtonField(super.rightProperty());
editableProperty().addListener((observable, oldValue, editable) -> {
if(!editable) {
setOnMouseClicked(copyHandler);
selectedTextProperty().addListener(selectionListener);
} else {
setOnMouseClicked(null);
selectedTextProperty().removeListener(selectionListener);
}
});
}
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(getCopyText());
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();
}
});
}
protected String getCopyText() {
return getText();
}
}
@@ -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,78 @@
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;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
setContextMenu(null);
} else if(utxoEntry.getHashIndex().getDate() != null) {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
} else {
setText("Unknown");
setContextMenu(null);
}
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,35 @@
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) {
if(date == null) {
return "Unknown";
}
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,29 @@
package com.sparrowwallet.sparrow.control;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateStringConverter extends StringConverter<LocalDate> {
public static final String FORMAT_PATTERN = "yyyy/MM/dd";
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN);
@Override
public String toString(LocalDate date) {
if (date != null) {
return DATE_FORMATTER.format(date);
} else {
return "";
}
}
@Override
public LocalDate fromString(String string) {
if (string != null && !string.isEmpty()) {
return LocalDate.parse(string, DATE_FORMATTER);
} else {
return null;
}
}
}
@@ -0,0 +1,90 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
private Wallet wallet;
public void setWallet(Wallet wallet) {
clear();
this.wallet = wallet;
DescriptorContextMenu contextMenu = new DescriptorContextMenu(wallet, this);
setContextMenu(contextMenu);
PolicyType policyType = wallet.getPolicyType();
ScriptType scriptType = wallet.getScriptType();
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
for(Keystore keystore : keystores) {
append(",", "descriptor-text");
replace(getLength(), getLength(), keystore.getScriptName(), List.of(keystore.isValid() ? "descriptor-text" : "descriptor-error", keystore.getScriptName()));
}
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
}
public Wallet getWallet() {
return wallet;
}
public void clear() {
super.clear();
this.wallet = null;
setDisable(false);
setContextMenu(null);
}
private static class DescriptorContextMenu extends ContextMenu {
public DescriptorContextMenu(Wallet wallet, DescriptorArea descriptorArea) {
MenuItem copyvalue = new MenuItem("Copy Value");
copyvalue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(descriptorArea.getText());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyvalue);
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyOutputDescriptor);
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
}
}
}
@@ -0,0 +1,36 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet;
this.outputDescriptor = outputDescriptor;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(wallet, outputDescriptor, device, defaultDevice);
}
@Subscribe
public void addressDisplayed(AddressDisplayedEvent event) {
setResult(event.getAddress());
}
}
@@ -0,0 +1,146 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
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.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class DeviceDialog<R> extends Dialog<R> {
private final List<String> operationFingerprints;
private final Accordion deviceAccordion;
private final Button scanButton;
private final VBox scanBox;
private final Label scanLabel;
public DeviceDialog() {
this(null);
}
public DeviceDialog(List<String> operationFingerprints) {
this.operationFingerprints = operationFingerprints;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.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");
scanButton = new Button("Scan...");
scanButton.setPrefSize(120, 60);
scanButton.setOnAction(event -> {
scan();
});
scanBox.getChildren().addAll(usb, scanLabel, scanButton);
scanBox.managedProperty().bind(scanBox.visibleProperty());
stackPane.getChildren().addAll(anchorPane, scanBox);
List<Device> devices = AppServices.getDevices();
if(devices == null || devices.isEmpty()) {
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
} else {
Platform.runLater(() -> setDevices(devices));
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);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
}
private void scan() {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
enumerateService.setOnSucceeded(workerStateEvent -> {
scanButton.setText("Scan...");
List<Device> devices = enumerateService.getValue();
setDevices(devices);
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
scanButton.setText("Scan...");
deviceAccordion.getPanes().clear();
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
enumerateService.setOnRunning(workerStateEvent -> {
scanButton.setText("Scanning...");
});
enumerateService.start();
}
protected void setDevices(List<Device> devices) {
List<Device> dialogDevices = new ArrayList<>(devices);
dialogDevices.removeIf(Objects::isNull);
if(operationFingerprints != null) {
dialogDevices.removeIf(device -> {
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.isNeedsPinSent() || device.isNeedsPassphraseSent());
});
}
deviceAccordion.getPanes().clear();
if(dialogDevices.isEmpty()) {
scanButton.setDefaultButton(true);
scanBox.setVisible(true);
scanLabel.setText("No matching devices found");
} else {
scanBox.setVisible(false);
for(Device device : dialogDevices) {
DevicePane devicePane = getDevicePane(device, dialogDevices.size() == 1);
deviceAccordion.getPanes().add(devicePane);
}
}
}
protected abstract DevicePane getDevicePane(Device device, boolean defaultDevice);
}
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
private final Wallet masterWallet;
private final List<StandardAccount> availableAccounts;
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
super(operationFingerprints);
this.masterWallet = masterWallet;
this.availableAccounts = availableAccounts;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
}
@Subscribe
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
setResult(event.getDiscoveredKeystores());
}
}
@@ -0,0 +1,796 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
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.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
private final DeviceOperation deviceOperation;
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final List<StandardAccount> availableAccounts;
private final Device device;
private CustomPasswordField pinField;
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private ButtonBase importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = requiredDerivation;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createImportButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createSignButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createDisplayAddressButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createSignMessageButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
this.availableAccounts = availableAccounts;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createDiscoverKeystoresButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
}
private void initialise(Device device) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
unlockButton.setVisible(true);
} else if(device.isNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else if(device.getError() != null) {
setError("Error", device.getError());
Platform.runLater(() -> {
setExpanded(true);
});
} else {
showOperationButton();
}
}
@Override
protected Control createButton() {
createUnlockButton();
return unlockButton;
}
private void setDefaultStatus() {
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "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 = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
});
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
}
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private void createSignButton() {
signButton = new Button("Sign");
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.setAlignment(Pos.CENTER_RIGHT);
displayAddressButton.setOnAction(event -> {
displayAddressButton.setDisable(true);
displayAddress();
});
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false);
List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
displayAddressButton.setDisable(true);
}
}
private void createSignMessageButton() {
signMessageButton = new Button("Sign Message");
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
signMessageButton.setOnAction(event -> {
signMessageButton.setDisable(true);
signMessage();
});
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
signMessageButton.setDisable(true);
}
}
private void createDiscoverKeystoresButton() {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
discoverKeystoresButton.setVisible(false);
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
}
}
private Node getPinEntry() {
VBox vBox = new VBox();
vBox.setMaxHeight(120);
vBox.setSpacing(42);
pinField = new ViewPasswordField();
Platform.runLater(() -> pinField.requestFocus());
enterPinButton = new Button("Enter PIN");
enterPinButton.setDefaultButton(true);
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() {
CustomPasswordField passphraseField = new ViewPasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
passphraseField.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
sendPassphrase(passphrase.get());
});
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
sendPassphraseButton.setText("Send Passphrase");
sendPassphraseButton.getStyleClass().add("default-button");
sendPassphraseButton.setOnAction(event -> {
setExpanded(false);
setDescription("Confirm passphrase on device...");
sendPassphrase(passphrase.get());
});
MenuItem removePassphrase = new MenuItem("Toggle Passphrase Off");
removePassphrase.setOnAction(event -> {
setExpanded(false);
setDescription("Toggling passphrase off, check device...");
togglePassphraseOff();
});
sendPassphraseButton.getItems().add(removePassphrase);
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(passphraseField);
contentBox.getChildren().add(sendPassphraseButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
Platform.runLater(passphraseField::requestFocus);
return contentBox;
}
private Node getTogglePassphraseOn() {
CopyableLabel label = new CopyableLabel("Passphrase is currently disabled");
HBox.setHgrow(label, Priority.ALWAYS);
Button togglePassphraseOn = new Button("Toggle Passphrase On");
togglePassphraseOn.setOnAction(event -> {
setExpanded(false);
hideButtons(importButton, signButton, displayAddressButton, signMessageButton);
setDescription("Toggling passphrase on, check device...");
togglePassphraseOn();
});
HBox contentBox = new HBox();
contentBox.setSpacing(20);
contentBox.setAlignment(Pos.CENTER_LEFT);
contentBox.getChildren().addAll(label, togglePassphraseOn);
contentBox.setPadding(new Insets(10, 30, 10, 30));
return contentBox;
}
private void hideButtons(Node... buttons) {
for(Node button : buttons) {
if(button != null) {
button.setVisible(false);
}
}
}
private void promptPin() {
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
promptPinService.setOnSucceeded(workerStateEvent -> {
Boolean result = promptPinService.getValue();
if(result) {
setContent(getPinEntry());
setExpanded(true);
} else {
setError("Could not request PIN", null);
unlockButton.setDisable(false);
}
});
promptPinService.setOnFailed(workerStateEvent -> {
setError("Error", promptPinService.getException().getMessage());
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.isNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setContent(getPassphraseEntry());
setExpanded(true);
} else {
showOperationButton();
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
setContent(getTogglePassphraseOn());
}
}
} else {
setError("Incorrect PIN", null);
unlockButton.setDisable(false);
if(pinField != null) {
pinField.setText("");
}
}
});
sendPinService.setOnFailed(workerStateEvent -> {
setError("Error", sendPinService.getException().getMessage());
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);
setDescription("Passphrase sent");
showOperationButton();
} else {
setError("Passphrase send failed", null);
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
}
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
setPassphraseButton.setDisable(false);
setPassphraseButton.setVisible(true);
});
enumerateService.start();
}
private void togglePassphraseOff() {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
device.setNeedsPassphraseSent(false);
setPassphraseButton.setVisible(false);
setDescription("Unlocked");
showOperationButton();
});
togglePassphraseService.setOnFailed(workerStateEvent -> {
setError("Error", togglePassphraseService.getException().getMessage());
});
togglePassphraseService.start();
}
private void togglePassphraseOn() {
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
device.setNeedsPassphraseSent(true);
setPassphraseButton.setVisible(true);
setPassphraseButton.setDisable(true);
setDescription("Enter passphrase");
setContent(getPassphraseEntry());
setExpanded(true);
});
togglePassphraseService.setOnFailed(workerStateEvent -> {
setError("Error", togglePassphraseService.getException().getMessage());
});
togglePassphraseService.start();
}
private void importKeystore(List<ChildNumber> derivation) {
if(device.getFingerprint() == null) {
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
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("Error", enumerateService.getException().getMessage());
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();
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
});
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());
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
signButton.setDisable(false);
});
setDescription("Signing...");
showHideLink.setVisible(false);
signPSBTService.start();
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
});
displayAddressService.setOnFailed(failedEvent -> {
setError("Could not display address", displayAddressService.getException().getMessage());
displayAddressButton.setDisable(false);
});
setDescription("Check device for address");
displayAddressService.start();
}
private void signMessage() {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
signMessageService.setOnFailed(failedEvent -> {
setError("Could not sign message", signMessageService.getException().getMessage());
signMessageButton.setDisable(false);
});
setDescription("Signing message...");
signMessageService.start();
}
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
accountDerivationPaths.put(availableAccount, derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
walletDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
});
walletDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
walletDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
setDescription("Discovering...");
showHideLink.setVisible(false);
getXpubsService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
importButton.getStyleClass().add("default-button");
}
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setDefaultButton(defaultDevice);
displayAddressButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
signMessageButton.setDefaultButton(defaultDevice);
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false);
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import Custom Derivation");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importXpub(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
public Device getDevice() {
return device;
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
}
}
@@ -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<String> operationFingerprints, PSBT psbt) {
super(operationFingerprints);
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, boolean defaultDevice) {
return new DevicePane(psbt, device, defaultDevice);
}
@Subscribe
public void psbtSigned(PSBTSignedEvent event) {
if(psbt == event.getPsbt()) {
setResult(event.getSignedPsbt());
}
}
}
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceSignMessageDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final String message;
private final KeyDerivation keyDerivation;
public DeviceSignMessageDialog(List<String> operationFingerprints, Wallet wallet, String message, KeyDerivation keyDerivation) {
super(operationFingerprints);
this.wallet = wallet;
this.message = message;
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(wallet, message, keyDerivation, device, defaultDevice);
}
@Subscribe
public void messageSigned(MessageSignedEvent event) {
if(wallet == event.getWallet()) {
setResult(event.getSignature());
}
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.control;
import tornadofx.control.Form;
public class DynamicForm extends Form {
private DynamicUpdate dynamicUpdate;
public DynamicForm() {
super();
}
public void setDynamicUpdate(DynamicUpdate dynamicUpdate) {
this.dynamicUpdate = dynamicUpdate;
}
@Override
protected void layoutChildren() {
if(dynamicUpdate != null) {
dynamicUpdate.update();
}
super.layoutChildren();
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.control;
public interface DynamicUpdate {
void update();
}
@@ -0,0 +1,607 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class EntryCell extends TreeTableCell<Entry, Entry> {
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
public EntryCell() {
super();
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;
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
setText("Unconfirmed");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
} else {
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
}
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip);
HBox actionBox = new HBox();
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
});
actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
increaseFeeButton.setOnAction(event -> {
increaseFee(transactionEntry);
});
actionBox.getChildren().add(increaseFeeButton);
}
if(blockTransaction.getHeight() <= 0 && containsWalletOutputs(transactionEntry)) {
Button cpfpButton = new Button("");
cpfpButton.setGraphic(getIncreaseFeeCPFPGlyph());
cpfpButton.setOnAction(event -> {
createCpfp(transactionEntry);
});
actionBox.getChildren().add(cpfpButton);
}
setGraphic(actionBox);
} else if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
actionBox.getChildren().add(receiveButton);
if(canSignMessage(nodeEntry.getWallet())) {
Button signMessageButton = new Button("");
signMessageButton.setGraphic(getSignMessageGlyph());
signMessageButton.setOnAction(event -> {
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
actionBox.getChildren().add(signMessageButton);
}
setGraphic(actionBox);
if(nodeEntry.getWallet().isWhirlpoolChildWallet()) {
setText(address.toString().substring(0, 20) + "...");
setContextMenu(null);
setGraphic(new HBox());
}
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(hashIndexEntry.getHashIndex().toString());
setTooltip(tooltip);
HBox actionBox = new HBox();
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
});
actionBox.getChildren().add(viewTransactionButton);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
Button spendUtxoButton = new Button("");
spendUtxoButton.setGraphic(getSendGlyph());
spendUtxoButton.setOnAction(event -> {
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
});
actionBox.getChildren().add(spendUtxoButton);
}
setGraphic(actionBox);
}
}
}
private static void increaseFee(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.filter(TransactionInput::isReplaceByFeeEnabled)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList());
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
Collections.shuffle(walletUtxos);
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
//If there is insufficent change output, include another random UTXO so the fee can be increased
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
utxos.add(utxo);
changeTotal += utxo.getValue();
vSize += inputSize;
}
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
externalOutputs.removeAll(ourOutputs);
externalOutputs.addAll(consolidationOutputs);
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
try {
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(label);
if(matcher.matches()) {
String base = matcher.group(1);
if(matcher.groupCount() > 2 && matcher.group(3) != null) {
int count = Integer.parseInt(matcher.group(3)) + 1;
label = base + "(Replaced By Fee #" + count + ")";
} else {
label = base + "(Replaced By Fee #2)";
}
} else {
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
}
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
} catch(Exception e) {
log.error("Error creating RBF payment", e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
if(payments.isEmpty()) {
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
return;
}
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
}
private static Double getMaxFeeRate() {
if(AppServices.getTargetBlockFeeRates() == null || AppServices.getTargetBlockFeeRates().isEmpty()) {
return 100.0;
}
return AppServices.getTargetBlockFeeRates().values().iterator().next();
}
private static void createCpfp(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
.map(HashIndexEntry::getHashIndex)
.collect(Collectors.toList());
if(ourOutputs.isEmpty()) {
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
}
BlockTransactionHashIndex utxo = ourOutputs.get(0);
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
}
private static boolean canSignMessage(Wallet wallet) {
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
return transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.anyMatch(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT));
}
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.collect(Collectors.toList());
if(!utxoEntries.contains(hashIndexEntry)) {
utxoEntries = List.of(hashIndexEntry);
}
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
}
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() != Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
.map(e -> ((HashIndexEntry)e).getHashIndex())
.filter(ref -> ref.getStatus() == Status.FROZEN)
.collect(Collectors.toList());
utxos.forEach(ref -> ref.setStatus(null));
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
}
private static Glyph getViewTransactionGlyph() {
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
searchGlyph.setFontSize(12);
return searchGlyph;
}
private static Glyph getIncreaseFeeRBFGlyph() {
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
increaseFeeGlyph.setFontSize(12);
return increaseFeeGlyph;
}
private static Glyph getIncreaseFeeCPFPGlyph() {
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
cpfpGlyph.setFontSize(12);
return cpfpGlyph;
}
private static Glyph getReceiveGlyph() {
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
return receiveGlyph;
}
private static Glyph getSignMessageGlyph() {
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12);
return signMessageGlyph;
}
private static Glyph getSendGlyph() {
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.setFontSize(12);
return sendGlyph;
}
private static Glyph getCopyGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COPY);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getFreezeGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SNOWFLAKE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getUnfreezeGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SUN);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
getItems().add(viewTransaction);
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> {
hide();
increaseFee(transactionEntry);
});
getItems().add(increaseFee);
}
if(containsWalletOutputs(transactionEntry)) {
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
createCpfp.setOnAction(AE -> {
hide();
createCpfp(transactionEntry);
});
getItems().add(createCpfp);
}
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyTxid);
}
}
private static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
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(viewTransaction, copyDate, copyTxid, copyHeight);
}
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {
hide();
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
getItems().add(receiveToAddress);
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setGraphic(getSignMessageGlyph());
signVerifyMessage.setOnAction(AE -> {
hide();
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
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(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph());
viewTransaction.setOnAction(AE -> {
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), hashIndexEntry.getBlockTransaction()));
});
getItems().add(viewTransaction);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
MenuItem sendSelected = new MenuItem("Send Selected");
sendSelected.setGraphic(getSendGlyph());
sendSelected.setOnAction(AE -> {
hide();
sendSelectedUtxos(treeTableView, hashIndexEntry);
});
getItems().add(sendSelected);
}
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.getHashIndex().isSpent()) {
if(hashIndexEntry.getHashIndex().getStatus() == null || hashIndexEntry.getHashIndex().getStatus() != Status.FROZEN) {
MenuItem freezeUtxo = new MenuItem("Freeze UTXO");
freezeUtxo.setGraphic(getFreezeGlyph());
freezeUtxo.setOnAction(AE -> {
hide();
freezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(freezeUtxo);
} else {
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
unfreezeUtxo.setOnAction(AE -> {
hide();
unfreezeUtxo(treeTableView, hashIndexEntry);
});
getItems().add(unfreezeUtxo);
}
}
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
MenuItem copyHashIndex = new MenuItem(label);
copyHashIndex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(hashIndexEntry.getHashIndex().toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyHashIndex);
}
}
public static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming");
cell.getStyleClass().remove("negative-amount");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
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");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent");
}
}
}
}
}
@@ -0,0 +1,126 @@
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.text.DecimalFormatSymbols;
import java.util.Currency;
import java.util.Locale;
public class FiatLabel extends CopyableLabel {
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
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,228 @@
package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport;
import com.sparrowwallet.sparrow.io.ImportException;
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.ButtonBase;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
public abstract class FileImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
private final FileImport importer;
protected ButtonBase importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
private final boolean scannable;
private final boolean fileFormatAvailable;
protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl);
this.importer = importer;
this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable;
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
if(scannable && fileFormatAvailable) {
ToggleButton scanButton = new ToggleButton("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
scanButton.setGraphic(cameraGlyph);
scanButton.setOnAction(event -> {
scanButton.setSelected(false);
importQR();
});
ToggleButton fileButton = new ToggleButton("Import File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
importFile();
});
importButton = fileButton;
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(scanButton, fileButton);
return segmentedButton;
} else if(scannable) {
importButton = new Button("Scan...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
importButton.setGraphic(cameraGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importQR();
});
return importButton;
} else {
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", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("JSON", "*.json"),
new FileChooser.ExtensionFilter("TXT", "*.txt")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
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 {
try(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);
}
}
}
private void importQR() {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.wallets != null) {
wallets = result.wallets;
try {
importFile(importer.getName(), null, null);
} catch(ImportException e) {
setError("Import Error", e.getMessage());
}
} else if(result.payload != null) {
try {
importFile(importer.getName(), new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)), null);
} catch(Exception e) {
log.error("Error importing QR", e);
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) {
errorMessage = "QR contents were not in JSON format";
}
setError("Import Error", errorMessage);
}
} else if(result.exception != null) {
log.error("Error importing QR", result.exception);
setError("Import Error", result.exception.getMessage());
}
}
}
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
if(wallets != null) {
for(Wallet wallet : wallets) {
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(importer.getName().replace(" Multisig", ""));
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(importer.getWalletModel());
return keystore;
}
}
throw new ImportException("Script type " + scriptType.getDescription() + " is not supported in this QR. Check you are displaying the correct QR code.");
}
return null;
}
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
Button importEncryptedButton = new Button("Import");
importEncryptedButton.setDefaultButton(true);
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,37 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
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;
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = getScannedKeystore(wallet.getScriptType());
if(keystore == null) {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
}
@@ -0,0 +1,154 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
private final boolean scannable;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
if(scannable) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportQR() {
exportWallet(null);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(wallet);
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
if(exporter instanceof Sparrow) {
fileName = wallet.getMasterName();
}
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
exportWallet(file);
}
}
private void exportWallet(File file) {
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
Wallet copy = wallet.copy();
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
try {
exportWallet(file, decryptedWallet);
} finally {
decryptedWallet.clearPrivate();
}
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
setError("Export Error", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
exportWallet(file, wallet);
}
}
private void exportWallet(File file, Wallet exportWallet) {
try {
if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) {
exporter.exportWallet(exportWallet, outputStream);
EventManager.get().post(new WalletExportEvent(exportWallet));
}
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(exportWallet, outputStream);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
}
}
}
@@ -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 import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
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,156 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
private final KeystoreFileImport importer;
private String fileName;
private byte[] fileBytes;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.importer = importer;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
}
} else {
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
}
}
setContent(getScriptTypeEntry(scriptTypes));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public ScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importFileButton = new Button("Import");
importFileButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(scriptTypeComboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
if(e.getCause() instanceof JsonParseException) {
errorMessage = "File was not in JSON format";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, fieldBox, region, importFileButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -0,0 +1,67 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
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());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
getStyleClass().add("help-label");
Platform.runLater(() -> setTooltip(tooltip));
}
private static Glyph getHelpGlyph() {
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
glyph.getStyleClass().add("help-icon");
glyph.setFontSize(11);
return glyph;
}
public final StringProperty helpTextProperty() {
if(helpText == null) {
helpText = new SimpleStringProperty(this, "helpText", "");
}
return helpText;
}
private StringProperty helpText;
public final void setHelpText(String value) {
helpTextProperty().setValue(value.replace("\\n", "\n"));
}
public final String getHelpText() {
return helpText == null ? "" : helpText.getValue();
}
public ObjectProperty<Node> helpGraphicProperty() {
if(helpGraphicProperty == null) {
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
}
return helpGraphicProperty;
}
private ObjectProperty<Node> helpGraphicProperty;
public final void setHelpGraphic(Node graphic) {
helpGraphicProperty().setValue(graphic);
}
}
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.text.Font;
import com.sparrowwallet.sparrow.AppServices;
public class IdLabel extends CopyableLabel {
public IdLabel() {
@@ -9,6 +9,6 @@ public class IdLabel extends CopyableLabel {
public IdLabel(String text) {
super(text);
setFont(Font.font("Courier"));
setFont(AppServices.getMonospaceFont());
}
}
@@ -0,0 +1,60 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.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(walletName, keystore, false);
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = new ViewPasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
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);
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warnGlyph.getStyleClass().add("warn-icon");
warnGlyph.setFontSize(12);
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
warnLabel.setGraphicTextGap(5);
content.getChildren().add(warnLabel);
dialogPane.setContent(content);
Platform.runLater(passphrase::requestFocus);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
}
}
@@ -0,0 +1,109 @@
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.scene.input.DataFormat;
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 {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry);
setText(label);
setContextMenu(new LabelContextMenu(entry, 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(Entry entry, 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);
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(content instanceof String) {
MenuItem pasteLabel = new MenuItem("Paste Label");
pasteLabel.setOnAction(AE -> {
hide();
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
if(currentContent instanceof String) {
entry.labelProperty().set((String)currentContent);
}
});
getItems().add(pasteLabel);
}
}
}
}
@@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
public class MasterKeyDisplayDialog extends Dialog<Void> {
public MasterKeyDisplayDialog(Keystore decryptedKeystore) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
ScrollPane scrollPane = new ScrollPane();
scrollPane.getStyleClass().add("edge-to-edge");
scrollPane.setPrefHeight(200);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
Accordion keystoreAccordion = new Accordion();
scrollPane.setContent(keystoreAccordion);
XprvKeystoreImportPane keystorePane = new XprvKeystoreImportPane(decryptedKeystore);
keystorePane.setAnimated(false);
keystoreAccordion.getPanes().add(keystorePane);
stackPane.getChildren().addAll(anchorPane);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(260);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
}
}
@@ -0,0 +1,216 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.chart.*;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
public static final int MAX_PERIOD_HOURS = 2;
private static final double Y_VALUE_BREAK_MVB = 3.0;
private Tooltip tooltip;
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize() {
setCreateSymbols(false);
setCursor(Cursor.CROSSHAIR);
setVerticalGridLinesVisible(false);
tooltip = new Tooltip();
tooltip.setShowDelay(Duration.ZERO);
tooltip.setHideDelay(Duration.ZERO);
tooltip.setShowDuration(Duration.INDEFINITE);
Platform.runLater(() -> {
Node node = this.lookup(".plot-content");
Tooltip.install(node, tooltip);
});
}
public void update(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
getData().clear();
if(tooltip.isShowing()) {
tooltip.hide();
}
Map<Date, Set<MempoolRateSize>> periodRateSizes = getPeriodRateSizes(mempoolRateSizes);
Map<Date, String> categories = getCategories(periodRateSizes);
CategoryAxis categoryAxis = (CategoryAxis)getXAxis();
categoryAxis.setTickMarkVisible(false);
categoryAxis.setTickLabelGap(10);
categoryAxis.setAutoRanging(false);
categoryAxis.setCategories(FXCollections.observableArrayList(categories.values()));
categoryAxis.invalidateRange(new ArrayList<>(categories.values()));
categoryAxis.setGapStartAndEnd(false);
categoryAxis.setTickLabelRotation(0);
NumberAxis numberAxis = (NumberAxis)getYAxis();
this.setOnMouseMoved(mouseEvent -> {
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
if(category != null) {
Optional<String> time = categories.entrySet().stream().filter(entry -> entry.getValue().equals(category)).map(entry -> dateFormatter.format(entry.getKey())).findFirst();
time.ifPresent(s -> tooltip.setGraphic(new ChartTooltip(category, s, getData())));
}
});
long previousFeeRate = 0;
for(Long feeRate : SendController.FEE_RATES_RANGE) {
XYChart.Series<String, Number> series = new XYChart.Series<>();
series.setName(feeRate + "+ sats/vB");
long seriesTotalVSize = 0;
for(Date date : periodRateSizes.keySet()) {
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
long totalVSize = 0;
for(MempoolRateSize rateSize : rateSizes) {
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
totalVSize += rateSize.getVSize();
}
}
series.getData().add(new XYChart.Data<>(categories.get(date), totalVSize));
seriesTotalVSize += totalVSize;
}
if(seriesTotalVSize > 0) {
getData().add(series);
}
previousFeeRate = feeRate;
}
final double maxMvB = getMaxMvB(getData());
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
long vSizeBytes = object.longValue();
if(maxMvB > Y_VALUE_BREAK_MVB) {
return (vSizeBytes / (1000 * 1000)) + " MvB";
} else {
return (vSizeBytes / (1000)) + " kvB";
}
}
@Override
public Number fromString(String string) {
return null;
}
});
if(categories.keySet().iterator().hasNext()) {
String time = categories.values().iterator().next();
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
numberAxis.setTickLabelsVisible(true);
numberAxis.setOpacity(1);
} else {
numberAxis.setTickLabelsVisible(false);
numberAxis.setOpacity(0);
}
}
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
if(mempoolRateSizes.size() == 1) {
return mempoolRateSizes;
}
LocalDateTime period = LocalDateTime.now().minusHours(MAX_PERIOD_HOURS);
return mempoolRateSizes.entrySet().stream().filter(entry -> {
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
return dateTime.isAfter(period);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(u, v) -> { throw new IllegalStateException("Duplicate dates"); },
TreeMap::new));
}
private Map<Date, String> getCategories(Map<Date, Set<MempoolRateSize>> mempoolHistogram) {
Map<Date, String> categories = new LinkedHashMap<>();
String invisible = "" + '\ufeff';
for(Iterator<Date> iter = mempoolHistogram.keySet().iterator(); iter.hasNext(); ) {
Date date = iter.next();
String label = dateFormatter.format(date);
if(!categories.isEmpty() && iter.hasNext()) {
label = invisible;
invisible += '\ufeff';
}
categories.put(date, label);
}
return categories;
}
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
double maxTotal = 0d;
for(Series<String, Number> series : seriesList) {
maxTotal = Math.max(maxTotal, getMaxMvB(series));
}
return maxTotal;
}
private static double getMaxMvB(Series<String, Number> series) {
double total = 0d;
for(XYChart.Data<String, Number> data : series.getData()) {
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
total += mvb;
}
return total;
}
private static class ChartTooltip extends VBox {
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
Label title = new Label("At " + time);
HBox titleBox = new HBox(title);
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
getChildren().add(titleBox);
double maxMvB = getMaxMvB(seriesList);
for(int i = seriesList.size() - 1; i >= 0; i--) {
Series<String, Number> series = seriesList.get(i);
for(XYChart.Data<String, Number> data : series.getData()) {
if(data.getXValue().equals(category)) {
double kvb = data.getYValue().doubleValue() / 1000;
double mvb = kvb / 1000;
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
Label label = new Label(series.getName() + ": " + amount);
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
if(i < 8) {
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
}
label.setGraphic(circle);
getChildren().add(label);
}
}
}
}
}
}
}
@@ -0,0 +1,420 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.security.SignatureException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final ToggleGroup formatGroup;
private final ToggleButton formatTrezor;
private final ToggleButton formatElectrum;
private final Wallet wallet;
private WalletNode walletNode;
private boolean electrumSignatureFormat;
private boolean closed;
/**
* Verification only constructor
*/
public MessageSignDialog() {
this(null, null);
}
/**
* Sign and verify with user entered address
*
* @param wallet Wallet to sign with
*/
public MessageSignDialog(Wallet wallet) {
this(wallet, null);
}
/**
* Sign and verify with preset address
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
this(wallet, walletNode, null, null);
}
/**
* Sign and verify with preset address, and supplied title, message and dialog buttons
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
* @param title Header text of dialog
* @param msg Message to sign (all fields will be made uneditable)
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
if(wallet != null) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
}
this.wallet = wallet;
this.walletNode = walletNode;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
Image image = new Image("image/seed.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
Field addressField = new Field();
addressField.setText("Address:");
address = new TextField();
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
addressField.getInputs().add(address);
if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString());
}
Field messageField = new Field();
messageField.setText("Message:");
message = new TextArea();
message.setWrapText(true);
message.setPrefRowCount(10);
message.setStyle("-fx-pref-height: 180px");
messageField.getInputs().add(message);
Field signatureField = new Field();
signatureField.setText("Signature:");
signature = new TextArea();
signature.getStyleClass().add("id");
signature.setPrefRowCount(2);
signature.setStyle("-fx-pref-height: 60px");
signature.setWrapText(true);
signatureField.getInputs().add(signature);
Field formatField = new Field();
formatField.setText("Format:");
formatGroup = new ToggleGroup();
formatElectrum = new ToggleButton("Standard (Electrum)");
formatTrezor = new ToggleButton("BIP137 (Trezor)");
SegmentedButton formatButtons = new SegmentedButton(formatElectrum, formatTrezor);
formatButtons.setToggleGroup(formatGroup);
formatField.getInputs().add(formatButtons);
formatGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
electrumSignatureFormat = (newValue == formatElectrum);
});
formatButtons.setDisable(wallet != null && walletNode != null && wallet.getScriptType() == ScriptType.P2PKH);
fieldset.getChildren().addAll(addressField, messageField, signatureField, formatField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
if(msg != null) {
message.setText(msg);
address.setEditable(false);
message.setEditable(false);
signature.setEditable(false);
formatButtons.setDisable(true);
}
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
if(buttons.length > 0) {
dialogPane.getButtonTypes().addAll(buttons);
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
if(customSignButtonType != null) {
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
customSignButton.setDefaultButton(true);
customSignButton.setOnAction(event -> {
customSignButton.setDisable(true);
signMessage();
setResult(ButtonBar.ButtonData.OK_DONE);
});
}
} else {
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
signButton.setDisable(wallet == null);
signButton.setOnAction(event -> {
signMessage();
});
Button verifyButton = (Button) dialogPane.lookupButton(verifyButtonType);
verifyButton.setDefaultButton(false);
verifyButton.setOnAction(event -> {
verifyMessage();
});
boolean validAddress = isValidAddress();
signButton.setDisable(!validAddress || (wallet == null));
verifyButton.setDisable(!validAddress);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = isValidAddress();
signButton.setDisable(!valid || (wallet == null));
verifyButton.setDisable(!valid);
if(valid && wallet != null) {
try {
Address address = getAddress();
setWalletNodeFromAddress(wallet, address);
} catch(InvalidAddressException e) {
//can't happen
}
}
});
}
EventManager.get().register(this);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.APPLY.equals(getResult())) {
event.consume();
return;
}
if(!closed) {
EventManager.get().unregister(this);
closed = true;
}
});
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
Platform.runLater(() -> {
if(address.getText().isEmpty()) {
address.requestFocus();
} else if(message.getText().isEmpty()) {
message.requestFocus();
}
formatGroup.selectToggle(formatElectrum);
});
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
public String getSignature() {
return signature.getText();
}
/**
* Use the Electrum signing format, which uses the non-segwit compressed signing parameters for both segwit types (p2sh-p2wpkh and p2wpkh)
*
* @param electrumSignatureFormat
*/
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
formatGroup.selectToggle(electrumSignatureFormat ? formatElectrum : formatTrezor);
this.electrumSignatureFormat = electrumSignatureFormat;
}
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType() != ScriptType.P2TR;
} catch (InvalidAddressException e) {
return false;
}
}
private void setWalletNodeFromAddress(Wallet wallet, Address address) {
walletNode = wallet.getWalletAddresses().get(address);
}
private void signMessage() {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
if(wallet.containsPrivateKeys()) {
if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
signUnencryptedKeystore(wallet);
}
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
}
}
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
signature.clear();
signature.appendText(signatureText);
privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
}
}
private void signUsbKeystore(Wallet usbWallet) {
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
signature.clear();
signature.appendText(optSignature.get());
}
}
private void verifyMessage() {
try {
//Find ECKey from message and signature
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
boolean verified = false;
try {
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText().trim(), signature.getText().trim(), false);
verified = verifyMessage(signedMessageKey);
} catch(SignatureException e) {
//ignore
}
if(!verified) {
try {
ECKey electrumSignedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText(), true);
verified = verifyMessage(electrumSignedMessageKey);
} catch(SignatureException e) {
//ignore
}
}
if(verified) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.setTitle("Verification Succeeded");
alert.setHeaderText("Verification Succeeded");
alert.setContentText("The signature verified against the message.");
AppServices.moveToActiveWindowScreen(alert);
alert.showAndWait();
} else {
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
}
} catch(IllegalArgumentException e) {
AppServices.showErrorDialog("Could not verify message", e.getMessage());
} catch(Exception e) {
log.error("Could not verify message", e);
AppServices.showErrorDialog("Could not verify message", e.getMessage());
}
}
private boolean verifyMessage(ECKey signedMessageKey) throws InvalidAddressException, SignatureException {
Address providedAddress = getAddress();
ScriptType scriptType = providedAddress.getScriptType();
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
Storage storage = event.getStorage(wallet);
if(storage == null) {
//Another window, ignore
return;
}
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
signUnencryptedKeystore(decryptedWallet);
decryptedWallet.clearPrivate();
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
}
}
@@ -0,0 +1,212 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
setContentDisplay(ContentDisplay.LEFT);
setGraphicTextGap(8);
getStyleClass().add("mixstatus-cell");
}
@Override
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
super.updateItem(mixStatus, empty);
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
if(empty || mixStatus == null) {
setText(null);
setGraphic(null);
} else {
setText(Integer.toString(mixStatus.getMixesDone()));
if(mixStatus.getNextMixUtxo() == null) {
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
} else {
setContextMenu(null);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
private void setMixSuccess(Utxo nextMixUtxo) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(-1);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason != MixFailReason.CANCEL) {
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) {
//Old error, don't set again.
return;
}
Glyph failGlyph = getFailGlyph();
setGraphic(failGlyph);
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
timeline.setOnFinished(event -> {
setTooltip(null);
});
timeline.play();
} else {
setGraphic(null);
setTooltip(null);
}
}
private String getPlatformSleepConfig() {
Platform platform = Platform.getCurrent();
if(platform == Platform.OSX) {
return "OSX System Preferences";
} else if(platform == Platform.WINDOWS) {
return "Windows Control Panel";
}
return "system power settings";
}
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
tt.setText(status);
setTooltip(tt);
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
tt.setOnShowing(event -> {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
registeredInputsService.setOnSucceeded(eventStateHandler -> {
if(registeredInputsService.getValue() != null) {
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
}
});
registeredInputsService.start();
});
}
} else {
setGraphic(null);
setTooltip(null);
}
}
private ProgressIndicator getProgressIndicator() {
ProgressIndicator progressIndicator;
if(getGraphic() instanceof ProgressIndicator) {
progressIndicator = (ProgressIndicator)getGraphic();
} else {
progressIndicator = new ProgressBar();
}
return progressIndicator;
}
private static Glyph getMixGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
copyGlyph.setFontSize(12);
return copyGlyph;
}
private static Glyph getStopGlyph() {
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
copyGlyph.setFontSize(12);
return copyGlyph;
}
public static Glyph getFailGlyph() {
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
failGlyph.getStyleClass().add("fail-warning");
failGlyph.setFontSize(12);
return failGlyph;
}
private static class MixStatusContextMenu extends ContextMenu {
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(isMixing) {
MenuItem mixStop = new MenuItem("Stop Mixing");
if(pool != null) {
mixStop.disableProperty().bind(pool.mixingProperty().not());
}
mixStop.setGraphic(getStopGlyph());
mixStop.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mixStop(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixStop);
} else {
MenuItem mixNow = new MenuItem("Mix Now");
if(pool != null) {
mixNow.disableProperty().bind(pool.mixingProperty().not());
}
mixNow.setGraphic(getMixGlyph());
mixNow.setOnAction(event -> {
hide();
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
if(whirlpool != null) {
try {
whirlpool.mix(utxoEntry.getHashIndex());
} catch(WhirlpoolException e) {
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
}
}
});
getItems().add(mixNow);
}
}
}
}
@@ -0,0 +1,620 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
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.input.Clipboard;
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.tools.Borders;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
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 nextButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
private SimpleListProperty<String> wordEntriesProperty;
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
private IntegerProperty defaultWordSizeProperty;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
createImportButton();
buttonBox.getChildren().add(importButton);
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
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("Enter 24 Words");
defaultWordSizeProperty = new SimpleIntegerProperty(24);
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
enterMnemonicButton.setText("Enter " + newValue + " Words");
});
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(defaultWordSizeProperty.get());
});
int[] numberWords = new int[] {24, 21, 18, 15, 12};
for(int i = 0; i < numberWords.length; i++) {
MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words");
final int words = numberWords[i];
item.setOnAction(event -> {
defaultWordSizeProperty.set(words);
enterMnemonic(words);
});
enterMnemonicButton.getItems().add(item);
}
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[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation, false);
});
importButton.getItems().add(item);
}
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.setVisible(false);
}
private 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);
List<WordEntry> wordEntries = new ArrayList<>(numWords);
for(int i = 0; i < numWords; i++) {
wordEntries.add(new WordEntry(i, wordEntryList));
}
for(int i = 0; i < numWords - 1; i++) {
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
}
wordsPane.getChildren().addAll(wordEntries);
vBox.getChildren().add(wordsPane);
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
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("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
calculateButton = new Button("Create Keystore");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Create the keystore 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);
nextButton = new Button("Confirm Backup...");
nextButton.setOnAction(event -> {
confirmRecord();
});
nextButton.managedProperty().bind(nextButton.visibleProperty());
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
nextButton.setVisible(false);
nextButton.setDefaultButton(true);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
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) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
invalidLabel.setText("Unsupported Electrum seed");
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
} else {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
}
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, nextButton, confirmButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
}
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
return stackPane;
}
private void generateNew() {
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstanceStrong();
} catch(NoSuchAlgorithmException e) {
secureRandom = new SecureRandom();
}
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
displayMnemonicCode();
}
private void displayMnemonicCode() {
setDescription("Write down words before re-entering");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
nextButton.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);
}
StackPane wordsStackPane = (StackPane)getContent();
if(wordsStackPane.getChildren().size() > 1) {
wordsStackPane.getChildren().remove(1);
confirmButton.setVisible(false);
}
}
private void confirmRecord() {
setDescription("Confirm words have been recorded");
showHideLink.setVisible(false);
StackPane wordsPane = (StackPane)getContent();
StackPane confirmPane = new StackPane();
confirmPane.setMaxWidth(350);
confirmPane.setMaxHeight(100);
Region region = new Region();
region.setMinWidth(confirmPane.getMaxWidth());
region.setMinHeight(confirmPane.getMaxHeight());
confirmPane.getStyleClass().add("box-overlay");
Node wrappedRegion = Borders.wrap(region).lineBorder().innerPadding(0).outerPadding(0).buildAll();
Label label = new Label("Have these " + wordEntriesProperty.get().size() + " words been written down?\nIn the next step, you will need to re-enter them.");
confirmPane.getChildren().addAll(wrappedRegion, label);
wordsPane.getChildren().add(confirmPane);
setExpanded(true);
backButton.setVisible(true);
nextButton.setVisible(false);
confirmButton.setVisible(true);
generateButton.setVisible(false);
}
private void confirmBackup() {
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(false);
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) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
} catch (ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
errorMessage = "Invalid word list - checksum incorrect";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
}
private static class WordEntry extends HBox {
private static List<String> wordList;
private final TextField wordField;
private WordEntry nextEntry;
private TextField nextField;
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
super();
setAlignment(Pos.CENTER_RIGHT);
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField() {
@Override
public void paste() {
Clipboard clipboard = Clipboard.getSystemClipboard();
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
String[] words = clipboard.getString().split("[\\n\\s]");
WordEntry entry = WordEntry.this;
for(String word : words) {
if(entry.nextField != null) {
entry.nextField.requestFocus();
}
entry.wordField.setText(word);
entry = entry.nextEntry;
if(entry == null) {
break;
}
}
} else {
super.paste();
}
}
};
wordField.setMaxWidth(100);
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText();
// if text was added, fix the text to fit the requirements
if(!text.isEmpty()) {
String newText = text.replace(" ", "").toLowerCase();
int carretPos = change.getCaretPosition() - text.length() + newText.length();
change.setText(newText);
// fix caret position based on difference in originally added text and fixed text
change.selectRange(carretPos, carretPos);
}
return change;
});
wordField.setTextFormatter(formatter);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
autoCompletionBinding.setOnAutoCompleted(event -> {
if(nextField != null) {
nextField.requestFocus();
}
});
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(wordField, Validator.combine(
Validator.createEmptyValidator("Word is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
));
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
wordEntryList.set(wordNumber, newValue);
});
this.getChildren().addAll(label, wordField);
}
public TextField getEditor() {
return wordField;
}
public void setNextEntry(WordEntry nextEntry) {
this.nextEntry = nextEntry;
}
public void setNextField(TextField field) {
this.nextField = field;
}
public static boolean isValid(String word) {
return wordList.contains(word);
}
}
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.startsWith(request.getUserText())) {
suggestions.add(word);
}
}
}
return suggestions;
}
}
private class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
public PassphraseEntry() {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label passphraseLabel = new Label("Passphrase:");
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);
}
public TextField getEditor() {
return passphraseField;
}
}
private Node getDerivationEntry(List<ChildNumber> derivation) {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(derivationField, Validator.combine(
Validator.createEmptyValidator("Derivation is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKeystore(importDerivation, false);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.TOP_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().add(derivationField);
contentBox.getChildren().add(importDerivationButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
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,130 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.wallet.bip47.rpc.PaymentCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.net.Proxy;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class PayNymAvatar extends StackPane {
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
public PayNymAvatar() {
super();
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
if(paymentCode == null) {
getChildren().clear();
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
String cacheId = getCacheId(paymentCode, getPrefWidth());
if(paymentCodeCache.containsKey(cacheId)) {
setImage(paymentCodeCache.get(cacheId));
} else {
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
payNymAvatarService.setOnRunning(runningEvent -> {
getChildren().clear();
});
payNymAvatarService.setOnSucceeded(successEvent -> {
setImage(payNymAvatarService.getValue());
});
payNymAvatarService.setOnFailed(failedEvent -> {
log.error("Error", failedEvent.getSource().getException());
});
payNymAvatarService.start();
}
}
});
}
private void setImage(Image image) {
getChildren().clear();
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
circle.setFill(new ImagePattern(image));
getChildren().add(circle);
}
public PaymentCode getPaymentCode() {
return paymentCodeProperty.get();
}
public ObjectProperty<PaymentCode> paymentCodeProperty() {
return paymentCodeProperty;
}
public void setPaymentCode(PaymentCode paymentCode) {
this.paymentCodeProperty.set(paymentCode);
}
private static String getCacheId(PaymentCode paymentCode, double width) {
return paymentCode.toString();
}
private static class PayNymAvatarService extends Service<Image> {
private final PaymentCode paymentCode;
private final double width;
public PayNymAvatarService(PaymentCode paymentCode, double width) {
this.paymentCode = paymentCode;
this.width = width;
}
@Override
protected Task<Image> createTask() {
return new Task<>() {
@Override
protected Image call() throws Exception {
String paymentCodeStr = paymentCode.toString();
String cacheId = getCacheId(paymentCode, width);
Object lock = paymentCodeLoading.get(cacheId);
if(lock != null) {
synchronized(lock) {
if(paymentCodeCache.containsKey(cacheId)) {
return paymentCodeCache.get(cacheId);
}
}
} else {
lock = new Object();
paymentCodeLoading.put(cacheId, lock);
}
synchronized(lock) {
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
Proxy proxy = AppServices.getProxy();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
paymentCodeCache.put(cacheId, image);
return image;
} catch(Exception e) {
log.debug("Error loading PayNym avatar", e);
throw e;
} finally {
lock.notifyAll();
}
}
}
};
}
}
}
@@ -0,0 +1,62 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.soroban.PayNym;
import com.sparrowwallet.sparrow.soroban.PayNymController;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
public class PayNymCell extends ListCell<PayNym> {
private final PayNymController payNymController;
public PayNymCell(PayNymController payNymController) {
super();
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("paynym-cell");
setPrefHeight(50);
this.payNymController = payNymController;
}
@Override
protected void updateItem(PayNym payNym, boolean empty) {
super.updateItem(payNym, empty);
if(empty || payNym == null) {
setText(null);
setGraphic(null);
} else {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(5, 5,5, 5));
PayNymAvatar payNymAvatar = new PayNymAvatar();
payNymAvatar.setPrefWidth(30);
payNymAvatar.setPrefHeight(30);
payNymAvatar.setPaymentCode(payNym.paymentCode());
HBox labelBox = new HBox();
labelBox.setAlignment(Pos.CENTER);
Label label = new Label(payNym.nymName(), payNymAvatar);
label.setGraphicTextGap(10);
labelBox.getChildren().add(label);
pane.setLeft(labelBox);
if(getListView().getUserData() == Boolean.TRUE) {
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
Button button = new Button("Add Contact");
hBox.getChildren().add(button);
pane.setRight(hBox);
button.setOnAction(event -> {
button.setDisable(true);
payNymController.followPayNym(payNym.paymentCode());
});
}
setText(null);
setGraphic(pane);
}
}
}
@@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.control;
import com.samourai.wallet.bip47.rpc.PaymentCode;
public class PaymentCodeTextField extends CopyableTextField {
private String paymentCodeStr;
public void setPaymentCode(PaymentCode paymentCode) {
this.paymentCodeStr = paymentCode.toString();
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
setText(abbrevPcode);
}
@Override
protected String getCopyText() {
return paymentCodeStr;
}
}
@@ -0,0 +1,53 @@
package com.sparrowwallet.sparrow.control;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ProgressIndicator;
import javafx.util.Duration;
public class ProgressTimer extends ProgressIndicator {
private final IntegerProperty secondsProperty = new SimpleIntegerProperty(60);
private Timeline timeline;
public ProgressTimer() {
super(0);
getStyleClass().add("progress-timer");
}
public void start() {
start(e -> {});
}
public void start(EventHandler<ActionEvent> onFinished) {
timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
new KeyFrame(Duration.seconds(getSeconds()), onFinished, new KeyValue(progressProperty(), 1)));
timeline.setCycleCount(1);
timeline.play();
}
public void stop() {
if(timeline != null) {
timeline.stop();
}
}
public int getSeconds() {
return secondsProperty.get();
}
public IntegerProperty secondsProperty() {
return secondsProperty;
}
public void setSeconds(int secondsProperty) {
this.secondsProperty.set(secondsProperty);
}
}
@@ -0,0 +1,245 @@
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.hummingbird.LegacyUREncoder;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@SuppressWarnings("deprecation")
public class QRDisplayDialog extends Dialog<UR> {
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
private static final int MIN_FRAGMENT_LENGTH = 10;
private static final int MAX_FRAGMENT_LENGTH = 250;
private static final int ANIMATION_PERIOD_MILLIS = 200;
private static final int QR_WIDTH = 480;
private static final int QR_HEIGHT = 480;
private final UR ur;
private final UREncoder encoder;
private final ImageView qrImageView;
private AnimateQRService animateQRService;
private String currentPart;
private boolean useLegacyEncoding;
private String[] legacyParts;
private int legacyPartIndex;
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
this(UR.fromBytes(type, data), addLegacyEncodingOption);
}
public QRDisplayDialog(UR ur) {
this(ur, false);
}
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
this.ur = ur;
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = new QRDisplayDialogPane();
setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
nextPart();
if(encoder.isSinglePart()) {
qrImageView.setImage(getQrCode(currentPart));
} else {
animateQRService = new AnimateQRService();
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
animateQRService.start();
setOnCloseRequest(event -> {
animateQRService.cancel();
});
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().add(cancelButtonType);
if(addLegacyEncodingOption) {
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
}
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
public QRDisplayDialog(String data) {
this.ur = null;
this.encoder = null;
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
qrImageView.setImage(getQrCode(data));
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
dialogPane.setPrefHeight(40 + QR_HEIGHT + 85);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
}
private void nextPart() {
if(!useLegacyEncoding) {
String fragment = encoder.nextPart();
currentPart = fragment.toUpperCase();
} else {
currentPart = legacyParts[legacyPartIndex];
legacyPartIndex++;
if(legacyPartIndex > legacyParts.length - 1) {
legacyPartIndex = 0;
}
}
}
private Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
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 void setUseLegacyEncoding(boolean useLegacyEncoding) {
if(useLegacyEncoding) {
try {
//Force to be bytes type for legacy encoding
LegacyUREncoder legacyEncoder = new LegacyUREncoder(new UR(RegistryType.BYTES.toString(), ur.getCborBytes()));
this.legacyParts = legacyEncoder.encode();
this.useLegacyEncoding = true;
if(legacyParts.length == 1) {
if(animateQRService != null) {
animateQRService.cancel();
}
nextPart();
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
} catch(UR.InvalidTypeException e) {
//Can't happen
}
} else {
this.useLegacyEncoding = false;
if(encoder.isSinglePart()) {
if(animateQRService != null) {
animateQRService.cancel();
}
qrImageView.setImage(getQrCode(currentPart));
} else if(!animateQRService.isRunning()) {
animateQRService.reset();
animateQRService.start();
}
}
}
private class AnimateQRService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws ImportException {
Image qrImage = getQrCode(currentPart);
qrImageView.setImage(qrImage);
nextPart();
return true;
}
};
}
}
private class QRDisplayDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton legacy = new ToggleButton(buttonType.getText());
legacy.setGraphicTextGap(5);
setLegacyGraphic(legacy, false);
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(legacy, buttonData);
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
setUseLegacyEncoding(newValue);
setLegacyGraphic(legacy, newValue);
});
return legacy;
}
return super.createButton(buttonType);
}
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
}
@@ -0,0 +1,820 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.LegacyURDecoder;
import com.sparrowwallet.hummingbird.registry.*;
import com.sparrowwallet.hummingbird.ResultType;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.URDecoder;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
@SuppressWarnings("deprecation")
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
private final URDecoder decoder;
private final LegacyURDecoder legacyDecoder;
private final WebcamService webcamService;
private List<String> parts;
private QRScanDialog.Result result;
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
private static final int SCAN_PERIOD_MILLIS = 100;
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
if(Config.get().isHdCapture()) {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService);
final DialogPane dialogPane = new QRScanDialogPane();
setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
ProgressBar progressBar = new ProgressBar();
progressBar.setMinHeight(20);
progressBar.setPadding(new Insets(0, 10, 0, 10));
progressBar.setPrefWidth(Integer.MAX_VALUE);
progressBar.progressProperty().bind(percentComplete);
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
}
Platform.runLater(() -> {
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
if(device.getName().equals(Config.get().getWebcamDevice())) {
webcamDeviceProperty.set(device);
}
}
}
});
});
VBox vBox = new VBox(20);
vBox.getChildren().addAll(wrappedView, progressBar);
dialogPane.setContent(vBox);
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
Throwable exception = failedEvent.getSource().getException();
Throwable nested = exception;
while(nested.getCause() != null) {
nested = nested.getCause();
}
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
Config.get().setWebcamDevice(null);
}
final Throwable result = exception;
Platform.runLater(() -> setResult(new Result(result)));
});
webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
}
webcamService.cancel();
});
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
Config.get().setWebcamDevice(newValue.getName());
if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel();
}
});
setOnCloseRequest(event -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
if(Config.get().isHdCapture() != isHdCapture) {
Config.get().setHdCapture(isHdCapture);
}
Platform.runLater(() -> webcamResolutionProperty.set(null));
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
}
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();
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
legacyDecoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
if(legacyDecoder.isComplete()) {
try {
UR ur = legacyDecoder.decode();
result = extractResultFromUR(ur);
} catch(Exception e) {
result = new Result(new URException(e.getMessage()));
}
}
} else {
decoder.receivePart(qrtext);
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
result = extractResultFromUR(urResult.ur);
} else {
result = new Result(new URException(urResult.error));
}
}
}
} else if(partMatcher.matches()) {
int m = Integer.parseInt(partMatcher.group(1));
int n = Integer.parseInt(partMatcher.group(2));
String payload = partMatcher.group(3);
if(parts == null) {
parts = new ArrayList<>(n);
IntStream.range(0, n).forEach(i -> parts.add(null));
}
parts.set(m - 1, payload);
if(n > 0) {
Platform.runLater(() -> percentComplete.setValue((double)parts.stream().filter(Objects::nonNull).count() / n));
}
if(parts.stream().filter(Objects::nonNull).count() == n) {
String complete = String.join("", parts);
try {
PSBT psbt = PSBT.fromString(complete, false);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
if(PSBT.isPSBT(complete)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
try {
Transaction transaction = new Transaction(Utils.hexToBytes(complete));
result = new Result(transaction);
return;
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
result = new Result(complete);
}
} else {
PSBT psbt;
Transaction transaction;
BitcoinURI bitcoinURI;
Address address;
ExtendedKey extendedKey;
try {
extendedKey = ExtendedKey.fromDescriptor(qrtext);
result = new Result(extendedKey);
return;
} catch(Exception e) {
//Ignore, not a valid xpub
}
try {
bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI);
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, false);
result = new Result(psbt);
return;
} catch(PSBTParseException e) {
if(PSBT.isPSBT(qrtext)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//Ignore, not parseable as Base64 or hex
}
try {
psbt = new PSBT(qrResult.getRawBytes(), false);
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(qrtext), false);
result = new Result(psbt);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
try {
transaction = new Transaction(Base43.decode(qrtext));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
result = new Result(qrtext);
}
}
private Result extractResultFromUR(UR ur) {
try {
RegistryType urRegistryType = ur.getRegistryType();
if(urRegistryType.equals(RegistryType.BYTES)) {
byte[] urBytes = (byte[])ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(urBytes, false);
return new Result(psbt);
} catch(PSBTParseException e) {
if(PSBT.isPSBT(urBytes)) {
log.error("Error parsing PSBT", e);
}
} catch(Exception e) {
//ignore, bytes not parsable as PSBT
}
try {
Transaction transaction = new Transaction(urBytes);
return new Result(transaction);
} catch(Exception e) {
//ignore, bytes not parsable as tx
}
try {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
ByteBuffer buf = ByteBuffer.wrap(urBytes);
CharBuffer charBuffer = decoder.decode(buf);
return new Result(charBuffer.toString());
} catch(Exception e) {
//ignore, bytes not parsable as utf-8
}
result = new Result(new URException("Parsed UR of type " + urRegistryType + " was not a PSBT, transaction or UTF-8 text"));
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
try {
PSBT psbt = new PSBT(cryptoPSBT.getPsbt(), false);
return new Result(psbt);
} catch(Exception e) {
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
}
} else if(urRegistryType.equals(RegistryType.CRYPTO_ADDRESS)) {
CryptoAddress cryptoAddress = (CryptoAddress)ur.decodeFromRegistry();
Address address = getAddress(cryptoAddress);
if(address != null) {
return new Result(BitcoinURI.fromAddress(address));
} else {
return new Result(new URException("Unknown " + urRegistryType + " type of " + cryptoAddress.getType()));
}
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
return new Result(extendedKey);
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
return new Result(outputDescriptor);
} else if(urRegistryType.equals(RegistryType.CRYPTO_ACCOUNT)) {
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
List<Wallet> wallets = getWallets(cryptoAccount);
return new Result(wallets);
} else {
log.error("Unsupported UR type " + urRegistryType);
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
}
} catch(IllegalArgumentException e) {
log.error("Unknown UR type of " + ur.getType(), e);
return new Result(new URException("Unknown UR type of " + ur.getType(), e));
} catch(UR.InvalidCBORException e) {
log.error("Invalid CBOR in UR", e);
return new Result(new URException("Invalid CBOR in UR", e));
} catch(Exception e) {
log.error("Error parsing UR CBOR", e);
return new Result(new URException("Error parsing UR CBOR", e));
}
return null;
}
private Address getAddress(CryptoAddress cryptoAddress) {
Address address = null;
if(cryptoAddress.getType() == null || cryptoAddress.getType() == CryptoAddress.Type.P2PKH) {
address = new P2PKHAddress(cryptoAddress.getData());
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2SH) {
address = new P2SHAddress(cryptoAddress.getData());
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2WPKH) {
address = new P2WPKHAddress(cryptoAddress.getData());
}
return address;
}
private ExtendedKey getExtendedKey(CryptoHDKey cryptoHDKey) {
if(cryptoHDKey.isPrivateKey()) {
DeterministicKey prvKey = HDKeyDerivation.createMasterPrivKeyFromBytes(Arrays.copyOfRange(cryptoHDKey.getKey(), 1, 33), cryptoHDKey.getChainCode(), List.of(ChildNumber.ZERO));
return new ExtendedKey(prvKey, new byte[4], ChildNumber.ZERO);
} else {
ChildNumber lastChild = ChildNumber.ZERO;
int depth = 1;
byte[] parentFingerprint = new byte[4];
if(cryptoHDKey.getOrigin() != null) {
if(!cryptoHDKey.getOrigin().getComponents().isEmpty()) {
PathComponent lastComponent = cryptoHDKey.getOrigin().getComponents().get(cryptoHDKey.getOrigin().getComponents().size() - 1);
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
depth = cryptoHDKey.getOrigin().getComponents().size();
}
if(cryptoHDKey.getParentFingerprint() != null) {
parentFingerprint = cryptoHDKey.getParentFingerprint();
}
}
DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint);
return new ExtendedKey(pubKey, parentFingerprint, lastChild);
}
}
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
if(cryptoOutput.getMultiKey() != null) {
MultiKey multiKey = cryptoOutput.getMultiKey();
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
for(CryptoHDKey cryptoHDKey : multiKey.getHdKeys()) {
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
KeyDerivation keyDerivation = getKeyDerivation(cryptoHDKey.getOrigin());
extendedPublicKeys.put(extendedKey, keyDerivation);
}
return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys);
} else if(cryptoOutput.getEcKey() != null) {
throw new IllegalArgumentException("EC keys are currently unsupported");
} else if(cryptoOutput.getHdKey() != null) {
ExtendedKey extendedKey = getExtendedKey(cryptoOutput.getHdKey());
KeyDerivation keyDerivation = getKeyDerivation(cryptoOutput.getHdKey().getOrigin());
return new OutputDescriptor(scriptType, extendedKey, keyDerivation);
}
throw new IllegalStateException("CryptoOutput did not contain sufficient information");
}
private List<Wallet> getWallets(CryptoAccount cryptoAccount) {
List<Wallet> wallets = new ArrayList<>();
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
Wallet wallet = new Wallet();
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
if(outputDescriptor.isMultisig()) {
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
}
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
wallet.setScriptType(outputDescriptor.getScriptType());
Keystore keystore = new Keystore();
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
keystore.setExtendedPublicKey(extendedKey);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(outputDescriptor.isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
wallets.add(wallet);
}
return wallets;
}
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
List<ScriptExpression> expressions = new ArrayList<>(scriptExpressions);
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG
|| expressions.get(expressions.size() - 1) == ScriptExpression.COSIGNER) {
expressions.remove(expressions.size() - 1);
}
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
return ScriptType.P2PKH;
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
return ScriptType.P2SH_P2WPKH;
} else if(List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
return ScriptType.P2WPKH;
} else if(List.of(ScriptExpression.SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2SH;
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2SH_P2WSH;
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
return ScriptType.P2WSH;
} else if(List.of(ScriptExpression.TAPROOT).equals(expressions)) {
return ScriptType.P2TR;
}
throw new IllegalArgumentException("Unknown script of " + expressions);
}
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
if(cryptoKeypath != null) {
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
}
return null;
}
}
private class QRScanListener implements WebcamListener {
@Override
public void webcamOpen(WebcamEvent webcamEvent) {
}
@Override
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
}
@Override
public void webcamDisposed(WebcamEvent webcamEvent) {
}
@Override
public void webcamImageObtained(WebcamEvent webcamEvent) {
}
}
private class QRScanDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
Node button = null;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton hd = new ToggleButton(buttonType.getText());
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
hd.setGraphicTextGap(5);
setHdGraphic(hd, hd.isSelected());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(hd, buttonData);
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
setHdGraphic(hd, newValue);
});
button = hd;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
devicesCombo.setConverter(new StringConverter<>() {
@Override
public String toString(WebcamDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
}
@Override
public WebcamDevice fromString(String string) {
throw new UnsupportedOperationException();
}
});
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
button = devicesCombo;
} else {
button = super.createButton(buttonType);
}
if(button instanceof Region) {
((Region)button).setPrefWidth(150);
((Region)button).setMaxWidth(150);
}
button.disableProperty().bind(webcamService.openingProperty());
return button;
}
private void setHdGraphic(ToggleButton hd, boolean isHd) {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
public final BitcoinURI uri;
public final ExtendedKey extendedKey;
public final OutputDescriptor outputDescriptor;
public final List<Wallet> wallets;
public final String payload;
public final Throwable exception;
public Result(Transaction transaction) {
this.transaction = transaction;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(PSBT psbt) {
this.transaction = null;
this.psbt = psbt;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(BitcoinURI uri) {
this.transaction = null;
this.psbt = null;
this.uri = uri;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(Address address) {
this.transaction = null;
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(ExtendedKey extendedKey) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = extendedKey;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(OutputDescriptor outputDescriptor) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = outputDescriptor;
this.wallets = null;
this.payload = null;
this.exception = null;
}
public Result(List<Wallet> wallets) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = wallets;
this.payload = null;
this.exception = null;
}
public Result(String payload) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = payload;
this.exception = null;
}
public Result(Throwable exception) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.payload = null;
this.exception = exception;
}
}
public static class ScanException extends Exception {
public ScanException() {
super();
}
public ScanException(String message) {
super(message);
}
public ScanException(Throwable cause) {
super(cause);
}
public ScanException(String message, Throwable cause) {
super(message, cause);
}
}
public static class URException extends ScanException {
public URException() {
super();
}
public URException(String message) {
super(message);
}
public URException(Throwable cause) {
super(cause);
}
public URException(String message, Throwable cause) {
super(message, cause);
}
}
public static class WebcamDependencyException extends ScanException {
public WebcamDependencyException() {
super();
}
public WebcamDependencyException(String message) {
super(message);
}
public WebcamDependencyException(Throwable cause) {
super(cause);
}
public WebcamDependencyException(String message, Throwable cause) {
super(message, cause);
}
}
public static class WebcamOpenException extends ScanException {
public WebcamOpenException() {
super();
}
public WebcamOpenException(String message) {
super(message);
}
public WebcamOpenException(Throwable cause) {
super(cause);
}
public WebcamOpenException(String message, Throwable cause) {
super(message, cause);
}
}
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
public long calculateDelay(long snapshotDuration, double deviceFps) {
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
}
}
}
@@ -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);
});
}
}
});
}
}

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