Compare commits

...

173 commits

Author SHA1 Message Date
Craig Raw
3a5fa69fb6 fix occasional issue with cell reuse when avoiding updating cells during table size estimation 2025-11-05 12:20:02 +02:00
Craig Raw
4774830ce4 add yu12 to supported pixel formats on linux 2025-11-05 07:51:26 +02:00
Craig Raw
2f62a9e9c8 show signing keystores in transaction blockchain form for spends from multisig wallets 2025-11-04 10:45:31 +02:00
doblon8
75bcfe2253
update jzbar dependency to 0.2.1 2025-10-24 09:38:11 +02:00
Craig Raw
bedf1399ea request display of path when retreiving xpubs on ledger devices for any non-standard path 2025-10-24 08:38:48 +02:00
Craig Raw
58575793ea update openpnp-capture to 0.0.30-1 2025-10-21 15:55:32 +02:00
Craig Raw
6c9b580d4f refactor to use transaction parameters record object when creating a wallet transaction 2025-10-21 12:06:00 +02:00
doblon8
31909b7a15
use language-independent sid for windows users group permission 2025-10-21 09:35:18 +02:00
Craig Raw
092267339a adapt to use declarative style to for consolidation payments 2025-10-17 10:27:20 +02:00
Craig Raw
0974918cff hide confirmations in tooltip when showing inputs and outputs on the transactions table 2025-10-16 08:42:55 +02:00
Craig Raw
0f4c36b3c2 add ctrl+shift+left/right keyboard shortcuts for moving tabs left and right 2025-10-13 14:26:37 +02:00
Craig Raw
e1fe35fb74 update nsmenufx to avoid npe 2025-10-06 14:52:24 +02:00
Craig Raw
d37fd00c4b avoid using deprecated camera device type constants on recent macos versions 2025-10-06 13:27:12 +02:00
Craig Raw
5f54f86df7 bump to v2.3.1 2025-10-03 11:53:27 +02:00
Craig Raw
e2fa3df08d restore pre gradle 9 archive task behaviour for file permissions 2025-10-03 10:09:57 +02:00
Craig Raw
6d6ede9abe bump to v2.3.0 2025-10-03 08:08:25 +02:00
Craig Raw
cca9ab1056 improve implementation of adding dns payment information from psbt 2025-10-02 14:58:02 +02:00
Craig Raw
9e33861110 revert to javafx 23 due to jpackage launcher link bug 2025-10-01 12:10:39 +02:00
Craig Raw
c3d3fd1fda revert to java 22 and javafx 24 due to bug in jpackage launcher linking (jdk-8345810) 2025-10-01 11:52:45 +02:00
Craig Raw
ca8553ecb8 revert continuity camera device change as unsupported on macos 13 2025-09-30 12:33:33 +02:00
Craig Raw
d23ee8c086 upgrade openpnp-capture to iterate over continuity camera devices on mac 2025-09-30 12:24:22 +02:00
Craig Raw
e776a17ad4 upgrade jdbi to remove older caffeine dependency 2025-09-30 09:37:45 +02:00
Craig Raw
480ce1e476 fix deprecation warning 2025-09-29 17:39:48 +02:00
Craig Raw
656cd90b08 upgrade guava and commons-lang3 2025-09-29 14:34:07 +02:00
Craig Raw
8df0777959 upgrade to java 25 and javafx 25 2025-09-29 13:32:56 +02:00
Craig Raw
84566b92e6 remove unnecessary zbar native libraries 2025-09-29 12:39:49 +02:00
Craig Raw
7802510e58 support dns hrns in send to many dialog 2025-09-29 11:53:52 +02:00
Craig Raw
efb1eb1051 add initial sending to silent payments support 2025-09-29 08:37:07 +02:00
Craig Raw
6240667478 improve error dialog on payjoin receiver error 2025-09-02 09:49:39 +02:00
Craig Raw
2c27112dad update drongo 2025-08-16 13:03:03 +02:00
Craig Raw
6d53e1ed1d fix bluewallet spelling 2025-08-12 08:09:01 +02:00
Craig Raw
e8c5660897 allow transaction diagram input and output labels to expand into available width 2025-08-11 14:03:51 +02:00
Craig Raw
bef6c750bd upgrade to gradle 8.14.3 2025-08-07 11:28:27 +02:00
Craig Raw
4ec3603789 fix non bip32 child derivation test 2025-08-07 08:55:19 +02:00
Craig Raw
90c9f9733f display a warning if an output descriptor provided in the wallet settings will be modified for use 2025-08-05 09:28:52 +02:00
Craig Raw
64efcf67d3 display zero byte length witness elements as empty instead of op_0 2025-08-04 13:38:41 +02:00
Craig Raw
385d173948 handle npe connecting to bitcoin core with wallet functionality disabled 2025-08-01 07:44:34 +02:00
Craig Raw
d81b868049 add any dns payment instructions from loaded psbts if not already cached 2025-07-31 11:47:43 +02:00
Craig Raw
2ff7a15d1e add padding to writes when connected over tls 2025-07-31 10:34:56 +02:00
Craig Raw
f48fa7e23c support zero in pin keypad for onekey classic pin entry 2025-07-29 14:45:30 +02:00
Craig Raw
4632850e1e use improved dnssec validation and handle offline state when resolving bip 353 hrns 2025-07-29 12:52:10 +02:00
Craig Raw
5f62523710 support sending to and displaying bip353 human readable names and include dnssec proof in associated psbts 2025-07-24 14:36:11 +02:00
Craig Raw
9dcf210762 support creating transactions with the minimum relay fee rate configured by the user or set by the connected server 2025-07-17 09:15:59 +02:00
Craig Raw
c7e9a0a161 restore coingecko historical rate support by limiting to the last 365 days 2025-07-15 09:06:52 +02:00
Craig Raw
fa10714844 followup 2025-07-10 08:40:31 +02:00
Craig Raw
80105aee62 save webcam device unique id instead of name to config 2025-07-10 08:15:00 +02:00
Craig Raw
3c5fa58a16 suppress warnings for jzbar ffm usage 2025-07-09 12:46:57 +02:00
doblon8
2a2be2617c
replace jni-based zbar wrapper with ffm-based jzbar
* Replace ZBar JNI library implementation with jzbar

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

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

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

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

* [feature/1630] Button positioning improvements

* [feature/1630] Added owner to qrDisplayDialog
2025-02-13 09:05:11 +02:00
Craig Raw
d0c827c2c7 fix various minor issues around multisig keystore labelling and export button visibility 2025-02-13 08:43:55 +02:00
Craig Raw
5c29bf51b7 handle scanning and pasting server urls in the electrum format 2025-02-11 14:03:43 +02:00
Craig Raw
d426703dcc fix account discovery on bitbox02 2025-02-11 13:18:21 +02:00
Craig Raw
78f0721168 bump to v2.1.3 2025-02-08 16:12:38 +02:00
Craig Raw
20d3f07059 draft implementation of optional bip329 fields 2025-02-08 11:43:59 +02:00
467 changed files with 7857 additions and 3414 deletions

View file

@ -10,13 +10,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [windows-2022, ubuntu-20.04, ubuntu-22.04-arm, macos-13, macos-14] os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-13, macos-14]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true
- name: Set up JDK 22.0.2 - name: Set up JDK 22.0.2
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '22.0.2' java-version: '22.0.2'
@ -30,7 +30,10 @@ jobs:
- name: Package tar distribution - name: Package tar distribution
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: ./gradlew packageTarDistribution run: ./gradlew packageTarDistribution
- name: Upload Artifacts - name: Repackage deb distribution
if: ${{ runner.os == 'Linux' }}
run: ./repackage.sh
- name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
@ -43,9 +46,9 @@ jobs:
- name: Package headless tar distribution - name: Package headless tar distribution
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: ./gradlew -Djava.awt.headless=true packageTarDistribution run: ./gradlew -Djava.awt.headless=true packageTarDistribution
- name: Rename Headless Artifacts - name: Repackage headless deb distribution
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done; run: ./repackage.sh
- name: Upload Headless Artifact - name: Upload Headless Artifact
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -1,50 +1,34 @@
plugins { plugins {
id 'application' id 'application'
id 'org-openjfx-javafxplugin' id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.1.1' id 'org.beryx.jlink' version '3.1.3'
id 'org.gradlex.extra-java-module-info' version '1.9' id 'org.gradlex.extra-java-module-info' version '1.13'
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
} }
def sparrowVersion = '2.1.2'
def os = org.gradle.internal.os.OperatingSystem.current() def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName() def osName = os.getFamilyName()
if(os.macOsX) { if(os.macOsX) {
osName = "osx" osName = "osx"
} }
def targetName = ""
def osArch = "x64" def osArch = "x64"
def releaseArch = "x86_64" def releaseArch = "x86_64"
if(System.getProperty("os.arch") == "aarch64") { if(System.getProperty("os.arch") == "aarch64") {
osArch = "aarch64" osArch = "aarch64"
releaseArch = "aarch64" releaseArch = "aarch64"
targetName = "-" + osArch
} }
def headless = "true".equals(System.getProperty("java.awt.headless")) def headless = "true".equals(System.getProperty("java.awt.headless"))
def vTor = '4.7.13-4' group = 'com.sparrowwallet'
def vKmpTor = '1.4.3' version = '2.3.1'
def kmpOs = osName
if(os.macOsX) {
kmpOs = "macos"
} else if(os.windows) {
kmpOs = "mingw"
}
def kmpArch = "x64"
if(System.getProperty("os.arch") == "aarch64") {
kmpArch = "arm64"
}
group "com.sparrowwallet"
version "${sparrowVersion}"
repositories { repositories {
mavenCentral() mavenCentral()
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' } maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
} }
tasks.withType(AbstractArchiveTask) { tasks.withType(AbstractArchiveTask).configureEach {
preserveFileTimestamps = false useFileSystemPermissions()
reproducibleFileOrder = true
} }
javafx { javafx {
@ -60,20 +44,20 @@ dependencies {
//Any changes to the dependencies must be reflected in the module definitions below! //Any changes to the dependencies must be reflected in the module definitions below!
implementation(project(':drongo')) implementation(project(':drongo'))
implementation(project(':lark')) implementation(project(':lark'))
implementation('com.google.guava:guava:33.0.0-jre') implementation('com.google.guava:guava:33.5.0-jre')
implementation('com.google.code.gson:gson:2.9.1') implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214') implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:4.0.3') { implementation('com.zaxxer:HikariCP:4.0.3') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.jdbi:jdbi3-core:3.20.0') { implementation('org.jdbi:jdbi3-core:3.49.5') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') { implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('org.flywaydb:flyway-core:9.22.3') implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.10.4') implementation('org.fxmisc.richtext:richtextfx:0.11.6')
implementation('no.tornado:tornadofx-controls:1.0.4') implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') { implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander' exclude group: 'com.beust', module: 'jcommander'
@ -89,34 +73,15 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2') implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4') implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9') implementation('co.nstant.in:cbor:0.9')
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") { implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
exclude group: 'com.google.android.tools', module: 'dx' implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
} implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") { implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
} }
if(kmpOs == "linux" && kmpArch == "arm64") { implementation('de.jangassen:nsmenufx:3.1.0') {
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") { exclude group: 'net.java.dev.jna', module: 'jna'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
} else {
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
} }
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}") {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.1.0' ) { implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics' exclude group: 'org.openjfx', module: 'javafx-graphics'
@ -136,7 +101,7 @@ dependencies {
implementation('com.sparrowwallet:tern:1.0.6') implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7') implementation('org.apache.commons:commons-lang3:3.19.0')
implementation('org.apache.commons:commons-compress:1.27.1') implementation('org.apache.commons:commons-compress:1.27.1')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0') implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30') implementation('com.github.librepdf:openpdf:1.3.30')
@ -145,6 +110,7 @@ dependencies {
implementation('com.github.hervegirod:fxsvgimage:1.1') implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0') implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3') implementation('com.jcraft:jzlib:1.1.3')
implementation('io.github.doblon8:jzbar:0.2.1')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0') testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher') testRuntimeOnly('org.junit.platform:junit-platform-launcher')
@ -177,6 +143,12 @@ application {
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet' mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError", applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
"--enable-native-access=com.sparrowwallet.drongo",
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=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.behavior=org.controlsfx.controls",
@ -186,11 +158,6 @@ application {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--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=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml", "--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.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
@ -201,8 +168,7 @@ application {
"--add-reads=org.flywaydb.core=java.desktop"] "--add-reads=org.flywaydb.core=java.desktop"]
if(os.macOsX) { if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png", applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
} }
if(headless) { if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"] applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
@ -225,7 +191,14 @@ jlink {
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*'] options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
launcher { launcher {
name = 'sparrow' name = 'sparrow'
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
"--enable-native-access=com.sun.jna",
"--enable-native-access=javafx.graphics",
"--enable-native-access=com.sparrowwallet.merged.module",
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=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.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
@ -234,11 +207,6 @@ jlink {
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow", "--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=com.sparrowwallet.sparrow",
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml", "--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.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
@ -256,6 +224,8 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor", "--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg", "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider", "--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core", "--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
"--add-reads=org.flywaydb.core=java.desktop"] "--add-reads=org.flywaydb.core=java.desktop"]
@ -273,7 +243,7 @@ jlink {
jpackage { jpackage {
imageName = "Sparrow" imageName = "Sparrow"
installerName = "Sparrow" installerName = "Sparrow"
appVersion = "${sparrowVersion}" appVersion = "${version}"
skipInstaller = os.macOsX || properties.skipInstallers skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = [] imageOptions = []
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE'] installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
@ -284,11 +254,13 @@ jlink {
} }
if(os.linux) { if(os.linux) {
if(headless) { if(headless) {
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"] installerName = "sparrowserver"
installerOptions = ['--license-file', 'LICENSE']
} else { } else {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow'] installerName = "sparrowwallet"
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
} }
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com'] installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/'] imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
} }
if(os.macOsX) { if(os.macOsX) {
@ -306,13 +278,15 @@ jlink {
if(os.linux) { if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules') tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
} else { } else {
tasks.jlink.finalizedBy('addUserWritePermission') tasks.jlink.finalizedBy('addUserWritePermission')
} }
tasks.register('addUserWritePermission', Exec) { tasks.register('addUserWritePermission', Exec) {
if(os.windows) { if(os.windows) {
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T' def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
} else { } else {
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal" commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
} }
@ -324,12 +298,42 @@ tasks.register('copyUdevRules', Copy) {
include('*') include('*')
} }
tasks.register('prepareResourceDir', Copy) {
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
into(layout.buildDirectory.dir('deploy/package'))
include('*')
eachFile { file ->
if(file.name.equals('control') || file.name.endsWith('.spec')) {
filter { line ->
if(line.contains('${size}')) {
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
}
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
}
}
}
}
static def getDirectorySize(File directory) {
long size = 0
if(directory.isFile()) {
size = directory.length()
} else if(directory.isDirectory()) {
directory.eachFileRecurse { file ->
if(file.isFile()) {
size += file.length()
}
}
}
return Long.toString(size/1024 as long)
}
tasks.register('removeGroupWritePermission', Exec) { tasks.register('removeGroupWritePermission', Exec) {
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow" commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
} }
tasks.register('packageZipDistribution', Zip) { tasks.register('packageZipDistribution', Zip) {
archiveFileName = "Sparrow-${sparrowVersion}.zip" archiveFileName = "Sparrow-${version}.zip"
destinationDirectory = file("$buildDir/jpackage") destinationDirectory = file("$buildDir/jpackage")
preserveFileTimestamps = os.macOsX preserveFileTimestamps = os.macOsX
from("$buildDir/jpackage/") { from("$buildDir/jpackage/") {
@ -340,7 +344,7 @@ tasks.register('packageZipDistribution', Zip) {
tasks.register('packageTarDistribution', Tar) { tasks.register('packageTarDistribution', Tar) {
dependsOn removeGroupWritePermission dependsOn removeGroupWritePermission
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz" archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
destinationDirectory = file("$buildDir/jpackage") destinationDirectory = file("$buildDir/jpackage")
compression = Compression.GZIP compression = Compression.GZIP
from("$buildDir/jpackage/") { from("$buildDir/jpackage/") {
@ -376,24 +380,11 @@ extraJavaModuleInfo {
requires('org.slf4j') requires('org.slf4j')
requires('com.fasterxml.jackson.databind') requires('com.fasterxml.jackson.databind')
} }
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') { module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
exports('org.bridj') exports('org.openpnp.capture')
exports('org.bridj.cpp') exports('org.openpnp.capture.library')
requires('java.logging')
}
module("com.github.sarxos:webcam-capture${targetName}", 'com.github.sarxos.webcam.capture') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
requires('java.desktop') requires('java.desktop')
requires('com.nativelibs4java.bridj') requires('com.sun.jna')
requires('org.slf4j')
}
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
exports('de.codecentric.centerdevice')
requires('javafx.base')
requires('javafx.controls')
requires('javafx.graphics')
} }
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') { module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader') exports('com.csvreader')
@ -401,21 +392,6 @@ extraJavaModuleInfo {
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture') module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305') module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8') module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
exports('org.jdbi.v3.core')
exports('org.jdbi.v3.core.mapper')
exports('org.jdbi.v3.core.statement')
exports('org.jdbi.v3.core.result')
exports('org.jdbi.v3.core.h2')
exports('org.jdbi.v3.core.spi')
requires('io.leangen.geantyref')
requires('java.sql')
requires('org.slf4j')
requires('com.github.benmanes.caffeine')
}
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
exports('io.leangen.geantyref')
}
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') { module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
exports('org.fxmisc.richtext') exports('org.fxmisc.richtext')
exports('org.fxmisc.richtext.event') exports('org.fxmisc.richtext.event')
@ -425,10 +401,10 @@ extraJavaModuleInfo {
requires('javafx.graphics') requires('javafx.graphics')
requires('org.fxmisc.flowless') requires('org.fxmisc.flowless')
requires('org.reactfx.reactfx') requires('org.reactfx.reactfx')
requires('org.fxmisc.undo.undofx') requires('org.fxmisc.undo')
requires('org.fxmisc.wellbehaved') requires('org.fxmisc.wellbehaved')
} }
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') { module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
requires('javafx.base') requires('javafx.base')
requires('javafx.controls') requires('javafx.controls')
requires('javafx.graphics') requires('javafx.graphics')
@ -485,119 +461,6 @@ extraJavaModuleInfo {
exports('net.coobird.thumbnailator') exports('net.coobird.thumbnailator')
requires('java.desktop') requires('java.desktop')
} }
module("io.matthewnelson.kotlin-components:kmp-tor-jvm", 'kmp.tor.jvm') {
exports('io.matthewnelson.kmp.tor')
requires('kmp.tor.binary.extract.jvm')
requires('kmp.tor.manager.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
requires('java.management')
}
if(kmpOs == "linux" && kmpArch == "arm64") {
module("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
} else {
module("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
exports("kmptor.${kmpOs}.${kmpArch}")
}
}
module("io.matthewnelson.kotlin-components:kmp-tor-binary-extract-jvm", 'kmp.tor.binary.extract.jvm') {
exports('io.matthewnelson.kmp.tor.binary.extract')
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
requires('kotlin.stdlib')
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
requires('kmp.tor.binary.geoip.jvm')
}
module("io.matthewnelson.kotlin-components:kmp-tor-manager-jvm", 'kmp.tor.manager.jvm') {
exports('io.matthewnelson.kmp.tor.manager')
exports('io.matthewnelson.kmp.tor.manager.util')
requires('kmp.tor.controller.common.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
requires('kotlinx.atomicfu')
requires('kmp.tor.controller.jvm')
requires('kmp.tor.common.jvm')
}
module("io.matthewnelson.kotlin-components:kmp-tor-manager-common-jvm", 'kmp.tor.manager.common.jvm') {
exports('io.matthewnelson.kmp.tor.manager.common')
exports('io.matthewnelson.kmp.tor.manager.common.event')
exports('io.matthewnelson.kmp.tor.manager.common.state')
requires('kmp.tor.controller.common.jvm')
requires('kmp.tor.common.jvm')
requires('kotlin.stdlib')
}
module("io.matthewnelson.kotlin-components:kmp-tor-controller-common-jvm", 'kmp.tor.controller.common.jvm') {
exports('io.matthewnelson.kmp.tor.controller.common.config')
exports('io.matthewnelson.kmp.tor.controller.common.file')
exports('io.matthewnelson.kmp.tor.controller.common.control')
exports('io.matthewnelson.kmp.tor.controller.common.control.usecase')
exports('io.matthewnelson.kmp.tor.controller.common.events')
exports('io.matthewnelson.kmp.tor.controller.common.exceptions')
requires('kmp.tor.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.atomicfu')
}
module("io.matthewnelson.kotlin-components:kmp-tor-common-jvm", 'kmp.tor.common.jvm') {
exports('io.matthewnelson.kmp.tor.common.address')
requires('parcelize.jvm')
requires('kotlin.stdlib')
}
module("io.matthewnelson.kotlin-components:kmp-tor-controller-jvm", 'kmp.tor.controller.jvm') {
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
requires('kmp.tor.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlinx.coroutines.core')
requires('kotlin.stdlib')
requires('kotlinx.atomicfu')
requires('encoding.core.jvm')
requires('encoding.base16.jvm')
}
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-common-jvm", 'kmp.tor.ext.callback.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.common')
}
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-jvm", 'kmp.tor.ext.callback.manager.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
requires('kmp.tor.manager.jvm')
requires('kmp.tor.ext.callback.common.jvm')
requires('kmp.tor.ext.callback.manager.common.jvm')
requires('kmp.tor.ext.callback.controller.common.jvm')
requires('kmp.tor.manager.common.jvm')
requires('kmp.tor.controller.common.jvm')
requires('kotlin.stdlib')
requires('kotlinx.coroutines.core')
}
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-common-jvm", 'kmp.tor.ext.callback.manager.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
requires('kmp.tor.ext.callback.controller.common.jvm')
}
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-controller-common-jvm", 'kmp.tor.ext.callback.controller.common.jvm') {
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
}
module("io.matthewnelson.kotlin-components:kmp-tor-binary-geoip-jvm", 'kmp.tor.binary.geoip.jvm') {
exports('io.matthewnelson.kmp.tor.binary.geoip')
exports('kmptor')
}
module("base16-jvm-2.0.0.jar", 'encoding.base16.jvm', "2.0.0") {
exports('io.matthewnelson.encoding.base16')
requires('encoding.core.jvm')
requires('kotlin.stdlib')
}
module("base32-jvm-2.0.0.jar", 'encoding.base32.jvm', "2.0.0")
module("base64-jvm-2.0.0.jar", 'encoding.base64.jvm', "2.0.0")
module("core-jvm-2.0.0.jar", 'encoding.core.jvm', "2.0.0") {
exports('io.matthewnelson.encoding.core')
requires('kotlin.stdlib')
}
module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") {
exports('io.matthewnelson.component.parcelize')
}
module('org.jcommander:jcommander', 'org.jcommander') { module('org.jcommander:jcommander', 'org.jcommander') {
exports('com.beust.jcommander') exports('com.beust.jcommander')
} }
@ -612,4 +475,8 @@ extraJavaModuleInfo {
module('com.jcraft:jzlib', 'com.jcraft.jzlib') { module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
exports('com.jcraft.jzlib') exports('com.jcraft.jzlib')
} }
}
kmpTorResourceFilterJar {
keepTorCompilation("current","current")
} }

View file

@ -4,13 +4,12 @@ plugins {
dependencies { dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3' implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
implementation 'org.javamodularity:moduleplugin:1.8.14'
} }
repositories { repositories {
mavenCentral() mavenCentral()
maven { maven {
url "https://plugins.gradle.org/m2/" url = uri("https://plugins.gradle.org/m2/")
} }
} }

View file

@ -32,7 +32,6 @@ package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetectorPlugin; import com.google.gradle.osdetector.OsDetectorPlugin;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask; import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> { public class JavaFXPlugin implements Plugin<Project> {
@ -40,10 +39,9 @@ public class JavaFXPlugin implements Plugin<Project> {
@Override @Override
public void apply(Project project) { public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class); project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project); project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().create("configJavafxRun", ExecTask.class, project); project.getTasks().register("configJavafxRun", ExecTask.class, project);
} }
} }

View file

@ -33,27 +33,19 @@ import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException; import org.gradle.api.GradleException;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
import org.openjfx.gradle.JavaFXModule; import org.openjfx.gradle.JavaFXModule;
import org.openjfx.gradle.JavaFXOptions; import org.openjfx.gradle.JavaFXOptions;
import org.openjfx.gradle.JavaFXPlatform; import org.openjfx.gradle.JavaFXPlatform;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.TreeSet; import java.util.TreeSet;
public class ExecTask extends DefaultTask { public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
private final Project project; private final Project project;
private JavaExec execTask; private JavaExec execTask;
@ -78,37 +70,11 @@ public class ExecTask extends DefaultTask {
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules()); var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
if (!definedJavaFXModuleNames.isEmpty()) { if (!definedJavaFXModuleNames.isEmpty()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter( final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName())) jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
); );
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform())); final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
if (moduleOptions != null) {
LOGGER.info("Modular JavaFX application found");
// Remove empty JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
} else {
LOGGER.info("Non-modular JavaFX application found");
// Remove all JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars);
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
var jvmArgs = new ArrayList<String>();
jvmArgs.add("--add-modules");
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
List<String> execJvmArgs = execTask.getJvmArgs();
if (execJvmArgs != null) {
jvmArgs.addAll(execJvmArgs);
}
jvmArgs.addAll(javaFXModuleJvmArgs);
execTask.setJvmArgs(jvmArgs);
}
} }
} else { } else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied"); throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");

View file

@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell ```shell
GIT_TAG="2.0.0" GIT_TAG="2.3.0"
``` ```
The project can then be initially cloned as follows: The project can then be initially cloned as follows:

2
drongo

@ -1 +1 @@
Subproject commit f7d5b4fb8fb0cbc2192a0f76fb4a1ef79a35d811 Subproject commit e975cbe6f8d8574785124e6db5780d0541e20024

Binary file not shown.

View file

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

15
gradlew vendored
View file

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

25
gradlew.bat vendored
View file

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

2
lark

@ -1 +1 @@
Subproject commit c4c7ca8da3a26e97c181a456eff5401b1bb7c1cc Subproject commit 10e8d9cd4bbe9fde4dd93c059e2a9faeec6be3e0

48
repackage.sh Executable file
View file

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

View file

@ -1,9 +0,0 @@
Package: sparrow
Version: 2.1.2-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
Architecture: arm64
Provides: sparrow
Description: Sparrow
Depends: libc6, zlib1g

View file

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

View file

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

View file

@ -1,9 +0,0 @@
Package: sparrow
Version: 2.1.2-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
Architecture: amd64
Provides: sparrow
Description: Sparrow
Depends: libc6, zlib1g

View file

@ -1,8 +1,8 @@
[Desktop Entry] [Desktop Entry]
Name=Sparrow Name=Sparrow
Comment=Sparrow Comment=Sparrow
Exec=/opt/sparrow/bin/Sparrow %U Exec=/opt/sparrowwallet/bin/Sparrow %U
Icon=/opt/sparrow/lib/Sparrow.png Icon=/opt/sparrowwallet/lib/Sparrow.png
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Finance;Network; Categories=Finance;Network;

View file

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

View file

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# postinst script for sparrow # postinst script for sparrowwallet
# #
# see: dh_installdeb(1) # see: dh_installdeb(1)
@ -22,9 +22,9 @@ package_type=deb
case "$1" in case "$1" in
configure) configure)
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then if ! getent group plugdev > /dev/null; then
groupadd plugdev groupadd plugdev
fi fi

View file

@ -1,19 +1,20 @@
Summary: Sparrow Summary: Sparrow
Name: sparrow Name: sparrowwallet
Version: 2.1.2 Version: ${version}
Release: 1 Release: 1
License: ASL 2.0 License: ASL 2.0
Vendor: Unknown Vendor: Unknown
%if "x" != "x" %if "x" != "x"
URL: URL: https://sparrowwallet.com
%endif %endif
%if "x/opt" != "x" %if "x/opt" != "x"
Prefix: /opt Prefix: /opt
%endif %endif
Provides: sparrow Provides: sparrowwallet
Obsoletes: sparrow <= 2.1.4
%if "xutils" != "x" %if "xutils" != "x"
Group: utils Group: utils
@ -40,7 +41,7 @@ Requires: xdg-utils
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib %define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
%description %description
Sparrow Sparrow Wallet
%global __os_install_post %{nil} %global __os_install_post %{nil}
@ -50,8 +51,8 @@ Sparrow
%install %install
rm -rf %{buildroot} rm -rf %{buildroot}
install -d -m 755 %{buildroot}/opt/sparrow install -d -m 755 %{buildroot}/opt/sparrowwallet
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
install -d -m 755 %{buildroot}/lib/systemd/system install -d -m 755 %{buildroot}/lib/systemd/system
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
@ -77,9 +78,9 @@ sed -i -e 's/.*/%dir "&"/' %{package_filelist}
%post %post
package_type=rpm package_type=rpm
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
if ! getent group plugdev > /dev/null; then if ! getent group plugdev > /dev/null; then
groupadd plugdev groupadd plugdev
fi fi
@ -251,9 +252,9 @@ desktop_trace ()
echo "$@" echo "$@"
} }
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
%clean %clean

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.1.2</string> <string>2.3.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories --> <!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@ -33,6 +33,8 @@
<string>Copyright (C) 2021</string> <string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<string>true</string> <string>true</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string> <string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>

View file

@ -1,17 +1,16 @@
package com.sparrowwallet.sparrow; package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander; import com.beust.jcommander.JCommander;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.google.common.io.ByteSource;
import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.*;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR; import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT; import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
@ -32,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm; import com.sparrowwallet.sparrow.wallet.WalletForm;
import de.codecentric.centerdevice.MenuToolkit; import de.jangassen.MenuToolkit;
import javafx.animation.*; import javafx.animation.*;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -51,12 +50,14 @@ import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.control.Label;
import javafx.scene.image.ImageView; import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.*; import javafx.stage.*;
import javafx.stage.Window;
import javafx.util.Duration; import javafx.util.Duration;
import org.controlsfx.control.Notifications; import org.controlsfx.control.Notifications;
import org.controlsfx.control.StatusBar; import org.controlsfx.control.StatusBar;
@ -71,6 +72,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.text.ParseException; import java.text.ParseException;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*; import static com.sparrowwallet.sparrow.AppServices.*;
@ -80,6 +82,7 @@ public class AppController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(AppController.class); private static final Logger log = LoggerFactory.getLogger(AppController.class);
public static final String DRAG_OVER_CLASS = "drag-over"; public static final String DRAG_OVER_CLASS = "drag-over";
public static final int TAB_LABEL_MAX_WIDTH = 300;
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8; public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95; public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view..."; public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
@ -381,7 +384,7 @@ public class AppController implements Initializable {
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty); openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses()); hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty); hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
useHdCameraResolutionProperty.set(Config.get().isHdCapture()); useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty); useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
mirrorCameraImageProperty.set(Config.get().isMirrorCapture()); mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty); mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
@ -573,16 +576,16 @@ public class AppController implements Initializable {
public void installUdevRules(ActionEvent event) { public void installUdevRules(ActionEvent event) {
String commands = """ String commands = """
sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
sudo udevadm control --reload sudo udevadm control --reload
sudo udevadm trigger sudo udevadm trigger
sudo groupadd -f plugdev sudo groupadd -f plugdev
sudo usermod -aG plugdev `whoami` sudo usermod -aG plugdev `whoami`
"""; """;
String home = System.getProperty(JPACKAGE_APP_PATH); String home = System.getProperty(JPACKAGE_APP_PATH);
if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) { if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
home = home.replace("bin/Sparrow", ""); home = home.replace("bin/Sparrow", "");
commands = commands.replace("/opt/sparrow/", home); commands = commands.replace("/opt/sparrowwallet/", home);
} }
TextAreaDialog dialog = new TextAreaDialog(commands, false); TextAreaDialog dialog = new TextAreaDialog(commands, false);
@ -823,10 +826,10 @@ public class AppController implements Initializable {
try(FileOutputStream outputStream = new FileOutputStream(file)) { try(FileOutputStream outputStream = new FileOutputStream(file)) {
if(asText) { if(asText) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs)); writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
writer.flush(); writer.flush();
} else { } else {
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true)); outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
} }
} catch(IOException e) { } catch(IOException e) {
log.error("Error saving PSBT", e); log.error("Error saving PSBT", e);
@ -849,7 +852,7 @@ public class AppController implements Initializable {
TabData tabData = (TabData)selectedTab.getUserData(); TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString(); String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
ClipboardContent content = new ClipboardContent(); ClipboardContent content = new ClipboardContent();
content.putString(data); content.putString(data);
@ -863,7 +866,7 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) { if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
byte[] psbtBytes = transactionTabData.getPsbt().serialize(); byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes); CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes); BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false); QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
@ -946,7 +949,11 @@ public class AppController implements Initializable {
public void useHdCameraResolution(ActionEvent event) { public void useHdCameraResolution(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource(); CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setHdCapture(item.isSelected()); if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
Config.get().setWebcamResolution(WebcamResolution.HD);
} else if(Config.get().getWebcamResolution().isWidescreenAspect() && !item.isSelected()) {
Config.get().setWebcamResolution(WebcamResolution.VGA);
}
} }
public void mirrorCameraImage(ActionEvent event) { public void mirrorCameraImage(ActionEvent event) {
@ -1031,6 +1038,10 @@ public class AppController implements Initializable {
cmd.add(System.getProperty(JPACKAGE_APP_PATH)); cmd.add(System.getProperty(JPACKAGE_APP_PATH));
cmd.addAll(args.toParams()); cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd); final ProcessBuilder builder = new ProcessBuilder(cmd);
if(OsType.getCurrent() == OsType.UNIX) {
Map<String, String> env = builder.environment();
env.remove("LD_LIBRARY_PATH");
}
builder.start(); builder.start();
quit(event); quit(event);
} catch(Exception e) { } catch(Exception e) {
@ -1254,6 +1265,10 @@ public class AppController implements Initializable {
} }
private void addImportedWallet(Wallet wallet) { private void addImportedWallet(Wallet wallet) {
if(AppServices.disallowAnyInvalidDerivationPaths(wallet)) {
return;
}
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate()); WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
nameDlg.initOwner(rootStack.getScene().getWindow()); nameDlg.initOwner(rootStack.getScene().getWindow());
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait(); Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
@ -1369,7 +1384,7 @@ public class AppController implements Initializable {
public void exportWallet(ActionEvent event) { public void exportWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm(); WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) { if(selectedWalletForm != null) {
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm); WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
dlg.initOwner(rootStack.getScene().getWindow()); dlg.initOwner(rootStack.getScene().getWindow());
Optional<Wallet> wallet = dlg.showAndWait(); Optional<Wallet> wallet = dlg.showAndWait();
if(wallet.isPresent()) { if(wallet.isPresent()) {
@ -1415,6 +1430,10 @@ public class AppController implements Initializable {
} }
public void sendToMany(ActionEvent event) { public void sendToMany(ActionEvent event) {
sendToMany(Collections.emptyList());
}
private void sendToMany(List<Payment> initialPayments) {
if(sendToManyDialog != null) { if(sendToManyDialog != null) {
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow(); Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true); stage.setAlwaysOnTop(true);
@ -1430,7 +1449,7 @@ public class AppController implements Initializable {
bitcoinUnit = wallet.getAutoUnit(); bitcoinUnit = wallet.getAutoUnit();
} }
sendToManyDialog = new SendToManyDialog(bitcoinUnit); sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
sendToManyDialog.initModality(Modality.NONE); sendToManyDialog.initModality(Modality.NONE);
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait(); Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
sendToManyDialog = null; sendToManyDialog = null;
@ -1471,6 +1490,7 @@ public class AppController implements Initializable {
stage.setAlwaysOnTop(true); stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false); stage.setAlwaysOnTop(false);
if(event.getSource() instanceof File file) { if(event.getSource() instanceof File file) {
downloadVerifierDialog.setInitialFile(file);
downloadVerifierDialog.setSignatureFile(file); downloadVerifierDialog.setSignatureFile(file);
} }
return; return;
@ -1881,6 +1901,11 @@ public class AppController implements Initializable {
} }
private void addTransactionTab(String name, File file, PSBT psbt) { private void addTransactionTab(String name, File file, PSBT psbt) {
//Convert to PSBTv0 first
if(psbt.getVersion() != null && psbt.getVersion() >= 2) {
psbt.convertVersion(0);
}
//Add any missing previous outputs if available in open wallets //Add any missing previous outputs if available in open wallets
for(PSBTInput psbtInput : psbt.getPsbtInputs()) { for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(psbtInput.getUtxo() == null) { if(psbtInput.getUtxo() == null) {
@ -1900,6 +1925,39 @@ public class AppController implements Initializable {
} }
} }
//Add DNS payment information if not already cached
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
try {
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
}
} catch(Exception e) {
log.debug("Error resolving DNS payment", e);
}
}
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
if(address != null && silentPaymentAddress == null) {
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
}
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
try {
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
}
} catch(Exception e) {
log.debug("Error resolving DNS payment", e);
}
}
}
}
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt); Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) { if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt)); EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
@ -1983,8 +2041,13 @@ public class AppController implements Initializable {
glyph.setFontSize(10.0); glyph.setFontSize(10.0);
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE); glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
Label tabLabel = new Label(tabName); Label tabLabel = new Label(tabName);
tabLabel.setMaxWidth(TAB_LABEL_MAX_WIDTH);
tabLabel.setGraphic(glyph); tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0); tabLabel.setGraphicTextGap(5.0);
if(TextUtils.computeTextWidth(tabLabel.getFont(), tabName, 0.0D) > TAB_LABEL_MAX_WIDTH) {
Tooltip tooltip = new Tooltip(tabName);
tabLabel.setTooltip(tooltip);
}
tab.setGraphic(tabLabel); tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab)); tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true); tab.setClosable(true);
@ -2033,23 +2096,33 @@ public class AppController implements Initializable {
} }
MenuItem moveRight = new MenuItem("Move Right"); MenuItem moveRight = new MenuItem("Move Right");
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveRight.setOnAction(event -> { moveRight.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab); int currentIndex = tabs.getSelectionModel().getSelectedIndex();
if(currentIndex + 1 >= tabs.getTabs().size()) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener); tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab); tabs.getTabs().remove(selectedTab);
tabs.getTabs().add(index + 1, tab); tabs.getTabs().add(currentIndex + 1, selectedTab);
tabs.getTabs().addListener(tabsChangeListener); tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab); tabs.getSelectionModel().select(selectedTab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
}); });
MenuItem moveLeft = new MenuItem("Move Left"); MenuItem moveLeft = new MenuItem("Move Left");
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
moveLeft.setOnAction(event -> { moveLeft.setOnAction(event -> {
int index = tabs.getTabs().indexOf(tab); int currentIndex = tabs.getSelectionModel().getSelectedIndex();
if(currentIndex == 0) {
return;
}
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
tabs.getTabs().removeListener(tabsChangeListener); tabs.getTabs().removeListener(tabsChangeListener);
tabs.getTabs().remove(tab); tabs.getTabs().remove(selectedTab);
tabs.getTabs().add(index - 1, tab); tabs.getTabs().add(currentIndex - 1, selectedTab);
tabs.getTabs().addListener(tabsChangeListener); tabs.getTabs().addListener(tabsChangeListener);
tabs.getSelectionModel().select(tab); tabs.getSelectionModel().select(selectedTab);
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
}); });
contextMenu.getItems().addAll(moveRight, moveLeft); contextMenu.getItems().addAll(moveRight, moveLeft);
@ -2621,7 +2694,6 @@ public class AppController implements Initializable {
} }
}); });
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
String walletName = event.getWallet().getFullDisplayName(); String walletName = event.getWallet().getFullDisplayName();
if(walletName.length() > 40) { if(walletName.length() > 40) {
walletName = walletName.substring(0, 40) + "..."; walletName = walletName.substring(0, 40) + "...";
@ -2630,10 +2702,10 @@ public class AppController implements Initializable {
Notifications notificationBuilder = Notifications.create() Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + walletName) .title("Sparrow - " + walletName)
.text(text) .text(text)
.graphic(new ImageView(image)) .graphic(new DialogImage(DialogImage.Type.SPARROW))
.hideAfter(Duration.seconds(15)) .hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT) .position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image))) .threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new DialogImage(DialogImage.Type.SPARROW)))
.onAction(e -> selectTab(event.getWallet())); .onAction(e -> selectTab(event.getWallet()));
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window) //If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
@ -2874,6 +2946,7 @@ public class AppController implements Initializable {
} }
} else if(event.isCompleted()) { } else if(event.isCompleted()) {
serverToggle.setDisable(false); serverToggle.setDisable(false);
statusBar.setProgress(0);
if(statusBar.getText().startsWith("Scanning...")) { if(statusBar.getText().startsWith("Scanning...")) {
statusBar.setText(""); statusBar.setText("");
} }
@ -3094,6 +3167,11 @@ public class AppController implements Initializable {
} }
} }
@Subscribe
public void requestSendToMany(RequestSendToManyEvent event) {
sendToMany(event.getPayments());
}
@Subscribe @Subscribe
public void functionAction(FunctionActionEvent event) { public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet()); selectTab(event.getWallet());
@ -3147,7 +3225,7 @@ public class AppController implements Initializable {
@Subscribe @Subscribe
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) { public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
useHdCameraResolutionProperty.set(event.isHdResolution()); useHdCameraResolutionProperty.set(event.getResolution().isWidescreenAspect());
} }
@Subscribe @Subscribe

View file

@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.crypto.Key; import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.control.DialogImage;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog; import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.Auth47; import com.sparrowwallet.sparrow.net.Auth47;
@ -25,6 +26,8 @@ import com.sparrowwallet.sparrow.control.TrayManager;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.net.*;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.subjects.PublishSubject;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -42,7 +45,6 @@ import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Dialog; import javafx.scene.control.Dialog;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.text.Font; import javafx.scene.text.Font;
import javafx.stage.Screen; import javafx.stage.Screen;
@ -66,6 +68,8 @@ import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*; import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
@ -87,8 +91,7 @@ public class AppServices {
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default"; private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50); public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L); private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
public static final double FALLBACK_FEE_RATE = 20000d / 1000; public static final double FALLBACK_FEE_RATE = 20000d / 1000;
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000; public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
@ -104,6 +107,8 @@ public class AppServices {
private TrayManager trayManager; private TrayManager trayManager;
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
private static Image windowIcon; private static Image windowIcon;
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false); private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
@ -126,12 +131,18 @@ public class AppServices {
private static BlockHeader latestBlockHeader; private static BlockHeader latestBlockHeader;
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
private static Map<Integer, Double> targetBlockFeeRates; private static Map<Integer, Double> targetBlockFeeRates;
private static Double nextBlockMedianFeeRate;
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>(); private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate; private static Double minimumRelayFeeRate;
private static Double serverMinimumRelayFeeRate;
private static CurrencyRate fiatCurrencyExchangeRate; private static CurrencyRate fiatCurrencyExchangeRate;
private static List<Device> devices; private static List<Device> devices;
@ -182,6 +193,12 @@ public class AppServices {
private AppServices(Application application, InteractionServices interactionServices) { private AppServices(Application application, InteractionServices interactionServices) {
this.application = application; this.application = application;
this.interactionServices = interactionServices; this.interactionServices = interactionServices;
newBlockSubject.buffer(4, TimeUnit.SECONDS)
.filter(newBlockEvents -> !newBlockEvents.isEmpty())
.observeOn(JavaFxScheduler.platform())
.subscribe(this::fetchBlockSummaries, exception -> log.error("Error fetching block summaries", exception));
EventManager.get().register(this); EventManager.get().register(this);
} }
@ -195,6 +212,7 @@ public class AppServices {
preventSleepService = createPreventSleepService(); preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener); onlineProperty.addListener(onlineServicesListener);
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
if(config.getMode() == Mode.ONLINE) { if(config.getMode() == Mode.ONLINE) {
if(config.requiresInternalTor()) { if(config.requiresInternalTor()) {
@ -261,7 +279,7 @@ public class AppServices {
} }
if(Tor.getDefault() != null) { if(Tor.getDefault() != null) {
Tor.getDefault().getTorManager().destroy(true, success -> {}); Tor.getDefault().close();
} }
} }
@ -291,12 +309,6 @@ public class AppServices {
if(event != null) { if(event != null) {
EventManager.get().post(event); EventManager.get().post(event);
} }
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
}
}); });
connectionService.setOnFailed(failEvent -> { connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try //Close connection here to create a new transport next time we try
@ -480,6 +492,26 @@ public class AppServices {
} }
} }
private void fetchFeeRates() {
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
feeRatesService = createFeeRatesService();
feeRatesService.start();
}
}
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
if(isConnected()) {
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
blockSummaryService.setOnSucceeded(_ -> {
EventManager.get().post(blockSummaryService.getValue());
});
blockSummaryService.setOnFailed(failedState -> {
log.error("Error fetching block summaries", failedState.getSource().getException());
});
blockSummaryService.start();
}
}
public static boolean isTorRunning() { public static boolean isTorRunning() {
return Tor.getDefault() != null; return Tor.getDefault() != null;
} }
@ -705,6 +737,10 @@ public class AppServices {
return latestBlockHeader; return latestBlockHeader;
} }
public static Map<Integer, BlockSummary> getBlockSummaries() {
return blockSummaries;
}
public static Double getDefaultFeeRate() { public static Double getDefaultFeeRate() {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget); return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
@ -716,6 +752,30 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE); return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
} }
public static List<Double> getLongFeeRatesRange() {
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return LONG_FEE_RATES_RANGE;
} else {
List<Double> longFeeRatesRange = new ArrayList<>();
longFeeRatesRange.add(minimumRelayFeeRate);
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
return longFeeRatesRange;
}
}
public static List<Double> getFeeRatesRange() {
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
} else {
List<Double> longFeeRatesRange = getLongFeeRatesRange();
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
}
}
public static Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
}
public static double getFallbackFeeRate() { public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE; return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
} }
@ -750,10 +810,18 @@ public class AppServices {
}); });
} }
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
}
public static Double getMinimumRelayFeeRate() { public static Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate; return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
} }
public static Double getServerMinimumRelayFeeRate() {
return serverMinimumRelayFeeRate;
}
public static CurrencyRate getFiatCurrencyExchangeRate() { public static CurrencyRate getFiatCurrencyExchangeRate() {
return fiatCurrencyExchangeRate; return fiatCurrencyExchangeRate;
} }
@ -767,8 +835,8 @@ public class AppServices {
} }
public static void addPayjoinURI(BitcoinURI bitcoinURI) { public static void addPayjoinURI(BitcoinURI bitcoinURI) {
if(bitcoinURI.getPayjoinUrl() == null) { if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
throw new IllegalArgumentException("Not a payjoin URI"); throw new IllegalArgumentException("Not a valid payjoin URI");
} }
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI); payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
} }
@ -1095,8 +1163,7 @@ public class AppServices {
walletChoiceDialog.initOwner(getActiveWindow()); walletChoiceDialog.initOwner(getActiveWindow());
walletChoiceDialog.setTitle("Choose Wallet"); walletChoiceDialog.setTitle("Choose Wallet");
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription); walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
Image image = new Image("/image/sparrow-small.png"); walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow()); setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
moveToActiveWindowScreen(walletChoiceDialog); moveToActiveWindowScreen(walletChoiceDialog);
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait(); Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
@ -1108,6 +1175,31 @@ public class AppServices {
return wallet; return wallet;
} }
public static boolean disallowAnyInvalidDerivationPaths(Wallet wallet) {
Optional<ScriptType> optInvalidScriptType = wallet.getKeystores().stream()
.filter(keystore -> keystore.getKeyDerivation() != null)
.map(keystore -> wallet.getOtherScriptTypeMatchingDerivation(keystore.getKeyDerivation().getDerivationPath()))
.filter(Optional::isPresent).map(Optional::get).findFirst();
if(optInvalidScriptType.isPresent()) {
ScriptType invalidScriptType = optInvalidScriptType.get();
boolean includePolicyType = !wallet.getScriptType().getAllowedPolicyTypes().getFirst().equals(invalidScriptType.getAllowedPolicyTypes().getFirst());
Optional<ButtonType> optType = AppServices.showWarningDialog("Invalid derivation path", "This wallet is using the derivation path for " +
invalidScriptType.getDescription(includePolicyType) + ", instead of the derivation path for its defined script type of " + wallet.getScriptType().getDescription(includePolicyType) +
". \n\nDisable derivation path validation to import this wallet?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent()) {
if(optType.get() == ButtonType.YES) {
Config.get().setValidateDerivationPaths(false);
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(true));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(true));
} else {
return true;
}
}
}
return false;
}
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET); public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
public static boolean isWhirlpoolCompatible(Wallet wallet) { public static boolean isWhirlpoolCompatible(Wallet wallet) {
@ -1123,7 +1215,8 @@ public class AppServices {
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) { public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get()) return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported && wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1; && wallet.getKeystores().size() == 1
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
} }
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) { public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
@ -1140,7 +1233,7 @@ public class AppServices {
} }
public static Font getMonospaceFont() { public static Font getMonospaceFont() {
return Font.font("Roboto Mono", 13); return Font.font("Fragment Mono Regular", 13);
} }
public static boolean isOnWayland() { public static boolean isOnWayland() {
@ -1156,9 +1249,22 @@ public class AppServices {
public void newConnection(ConnectionEvent event) { public void newConnection(ConnectionEvent event) {
currentBlockHeight = event.getBlockHeight(); currentBlockHeight = event.getBlockHeight();
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight)); System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE); if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
}
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
latestBlockHeader = event.getBlockHeader(); latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer(); Config.get().addRecentServer();
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
fetchFeeRates();
}
if(!blockSummaries.containsKey(currentBlockHeight)) {
fetchBlockSummaries(Collections.emptyList());
}
} }
@Subscribe @Subscribe
@ -1173,11 +1279,22 @@ public class AppServices {
latestBlockHeader = event.getBlockHeader(); latestBlockHeader = event.getBlockHeader();
String status = "Updating to new block height " + event.getHeight(); String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status)); EventManager.get().post(new StatusEvent(status));
newBlockSubject.onNext(event);
}
@Subscribe
public void blockSummary(BlockSummaryEvent event) {
blockSummaries.putAll(event.getBlockSummaryMap());
if(AppServices.currentBlockHeight != null) {
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
}
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
} }
@Subscribe @Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) { public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates(); targetBlockFeeRates = event.getTargetBlockFeeRates();
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
} }
@Subscribe @Subscribe
@ -1190,10 +1307,8 @@ public class AppServices {
@Subscribe @Subscribe
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
//Perform once-off fee rates retrieval to immediately change displayed rates //Perform once-off fee rates retrieval to immediately change displayed rates
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) { fetchFeeRates();
feeRatesService = createFeeRatesService(); fetchBlockSummaries(Collections.emptyList());
feeRatesService.start();
}
} }
@Subscribe @Subscribe

View file

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

View file

@ -72,10 +72,6 @@ public class SparrowDesktop extends Application {
Config.get().setServerType(ServerType.ELECTRUM_SERVER); Config.get().setServerType(ServerType.ELECTRUM_SERVER);
} }
if(Config.get().getHdCapture() == null && OsType.getCurrent() == OsType.MACOS) {
Config.get().setHdCapture(Boolean.TRUE);
}
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths())); System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths())); System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
@ -117,8 +113,8 @@ public class SparrowDesktop extends Application {
private void initializeFonts() { private void initializeFonts() {
GlyphFontRegistry.register(new FontAwesome5()); GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands()); GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13); Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11); Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
if(OsType.getCurrent() == OsType.MACOS) { if(OsType.getCurrent() == OsType.MACOS) {
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13); Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
} }

View file

@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet { public class SparrowWallet {
public static final String APP_ID = "sparrow"; public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow"; public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.1.2"; public static final String APP_VERSION = "2.3.1";
public static final String APP_VERSION_SUFFIX = ""; public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";

View file

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

View file

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

View file

@ -48,7 +48,7 @@ public class CardImportPane extends TitledDescriptionPane {
private final SimpleStringProperty pin = new SimpleStringProperty(""); private final SimpleStringProperty pin = new SimpleStringProperty("");
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) { public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
this.importer = importer; this.importer = importer;
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation(); this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
} }

View file

@ -87,6 +87,8 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
} else if(entry instanceof UtxoEntry) { } else if(entry instanceof UtxoEntry) {
setGraphic(null); setGraphic(null);
} else if(entry instanceof HashIndexEntry) { } else if(entry instanceof HashIndexEntry) {
tooltip.hideConfirmations();
Region node = new Region(); Region node = new Region();
node.setPrefWidth(10); node.setPrefWidth(10);
setGraphic(node); setGraphic(node);
@ -148,6 +150,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
setTooltipText(); setTooltipText();
} }
public void hideConfirmations() {
showConfirmations = false;
isCoinbase = false;
confirmationsProperty.unbind();
setTooltipText();
}
private void setTooltipText() { private void setTooltipText() {
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : "")); setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
} }

View file

@ -225,6 +225,13 @@ public class CoinTreeTable extends TreeTableView<Entry> {
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> { walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable()); event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
EventManager.get().post(event); EventManager.get().post(event);
//Reset pref widths here so window resizes don't cause reversion to previously set pref widths
Double[] widths = event.getWalletTable().getWidths();
for(int i = 0; i < getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = getColumns().get(i);
column.setPrefWidth(widths != null && getColumns().size() == widths.length ? widths[i] : STANDARD_WIDTH);
}
}); });
} }

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent; import javafx.scene.input.ClipboardContent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
public class CopyableCoinLabel extends CopyableLabel { public class CopyableCoinLabel extends CopyableLabel {
@ -29,6 +30,10 @@ public class CopyableCoinLabel extends CopyableLabel {
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit())); valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
setOnMouseClicked(event -> { setOnMouseClicked(event -> {
if(!event.getButton().equals(MouseButton.PRIMARY)) {
return;
}
if(bitcoinUnit == null) { if(bitcoinUnit == null) {
bitcoinUnit = Config.get().getBitcoinUnit(); bitcoinUnit = Config.get().getBitcoinUnit();
} }

View file

@ -75,7 +75,7 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice; private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) { public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.IMPORT; this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
@ -102,7 +102,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) { public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.SIGN; this.deviceOperation = DeviceOperation.SIGN;
this.wallet = wallet; this.wallet = wallet;
this.psbt = psbt; this.psbt = psbt;
@ -129,7 +129,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) { public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS; this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
@ -152,7 +152,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) { public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.SIGN_MESSAGE; this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
@ -179,7 +179,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) { public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES; this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
this.wallet = wallet; this.wallet = wallet;
this.psbt = null; this.psbt = null;
@ -202,7 +202,7 @@ public class DevicePane extends TitledDescriptionPane {
} }
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) { public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png"); super(device.getModel().toDisplayString(), "", "", device.getModel());
this.deviceOperation = deviceOperation; this.deviceOperation = deviceOperation;
this.wallet = null; this.wallet = null;
this.psbt = null; this.psbt = null;
@ -453,20 +453,26 @@ public class DevicePane extends TitledDescriptionPane {
}); });
vBox.getChildren().addAll(pinField, enterPinButton); vBox.getChildren().addAll(pinField, enterPinButton);
TilePane tilePane = new TilePane(); GridPane gridPane = new GridPane();
tilePane.setPrefColumns(3); gridPane.setHgap(10);
tilePane.setHgap(10); gridPane.setVgap(10);
tilePane.setVgap(10); gridPane.setMaxWidth(150);
tilePane.setMaxWidth(150); gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
tilePane.setMaxHeight(120);
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3}; int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
for(int i = 0; i < digits.length; i++) { for(int i = 0; i < digits.length; i++) {
Button pinButton = new Button(); Button pinButton = new Button();
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE"); Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
pinButton.setGraphic(circle); pinButton.setGraphic(circle);
pinButton.setUserData(digits[i]); pinButton.setUserData(digits[i]);
tilePane.getChildren().add(pinButton); GridPane.setRowIndex(pinButton, i / 3);
GridPane.setColumnIndex(pinButton, i % 3);
if((i / 3) == 3) {
GridPane.setHgrow(pinButton, Priority.ALWAYS);
GridPane.setColumnSpan(pinButton, 3);
pinButton.setMaxWidth(Double.MAX_VALUE);
}
gridPane.getChildren().add(pinButton);
pinButton.setOnAction(event -> { pinButton.setOnAction(event -> {
pinField.setText(pinField.getText() + pinButton.getUserData()); pinField.setText(pinField.getText() + pinButton.getUserData());
}); });
@ -474,7 +480,7 @@ public class DevicePane extends TitledDescriptionPane {
HBox contentBox = new HBox(); HBox contentBox = new HBox();
contentBox.setSpacing(50); contentBox.setSpacing(50);
contentBox.getChildren().add(tilePane); contentBox.getChildren().add(gridPane);
contentBox.getChildren().add(vBox); contentBox.getChildren().add(vBox);
contentBox.setPadding(new Insets(10, 0, 10, 0)); contentBox.setPadding(new Insets(10, 0, 10, 0));
contentBox.setAlignment(Pos.TOP_CENTER); contentBox.setAlignment(Pos.TOP_CENTER);

View file

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

View file

@ -56,13 +56,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt"); private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc"); private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg"); private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip"); private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "zip");
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz"); private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu"); private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z"); private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
private static final String SPARROW_RELEASE_PREFIX = "sparrow-"; private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc"; private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*"); private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024; private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
@ -70,6 +72,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>(); private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>(); private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>(); private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final ObjectProperty<File> initial = new SimpleObjectProperty<>();
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty(); private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty(); private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
@ -81,7 +84,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static File lastFileParent; private static File lastFileParent;
public DownloadVerifierDialog(File initialSignatureFile) { public DownloadVerifierDialog(File initialFile) {
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
@ -223,11 +226,17 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}); });
release.addListener((observable, oldValue, releaseFile) -> { release.addListener((observable, oldValue, releaseFile) -> {
if(releaseFile != null) {
initial.set(null);
}
verify(); verify();
}); });
if(initialSignatureFile != null) { if(initialFile != null) {
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile)); javafx.application.Platform.runLater(() -> {
initial.set(initialFile);
signature.set(initialFile);
});
} }
} }
@ -292,7 +301,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
publicKeyDisabled.set(true); publicKeyDisabled.set(true);
} }
if(manifest.get().equals(release.get())) { if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.get())) {
manifestDisabled.set(true); manifestDisabled.set(true);
releaseHash.setText("No hash required, signature signs release file directly"); releaseHash.setText("No hash required, signature signs release file directly");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph()); releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
@ -455,7 +464,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) { String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName()); Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) { if(matcher.find()) {
String version = matcher.group(); String version = matcher.group();
@ -482,6 +492,22 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
} }
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) { private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
File initialFile = initial.get();
if(initialFile != null && initialFile.exists()) {
for(File file : manifestMap.keySet()) {
if(initialFile.getName().equals(file.getName())) {
return initialFile;
}
}
List<List<String>> allExtensionLists = List.of(MACOS_RELEASE_EXTENSIONS, WINDOWS_RELEASE_EXTENSIONS, LINUX_RELEASE_EXTENSIONS, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS);
for(List<String> extensions : allExtensionLists) {
if(extensions.stream().anyMatch(ext -> initialFile.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
return initialFile;
}
}
}
List<String> releaseExtensions = getReleaseFileExtensions(); List<String> releaseExtensions = getReleaseFileExtensions();
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of("")); List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
@ -565,7 +591,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) { if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name); Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
return matcher.find(); return matcher.find();
} }
@ -574,10 +600,18 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
return false; return false;
} }
public static boolean isSparrowManifest(File manifestFile) {
return manifestFile.getName().startsWith(SPARROW_RELEASE_PREFIX) && manifestFile.getName().endsWith(SPARROW_MANIFEST_SUFFIX);
}
public void setSignatureFile(File signatureFile) { public void setSignatureFile(File signatureFile) {
signature.set(signatureFile); signature.set(signatureFile);
} }
public void setInitialFile(File initialFile) {
initial.set(initialFile);
}
private static class Header extends GridPane { private static class Header extends GridPane {
public Header() { public Header() {
setMaxWidth(Double.MAX_VALUE); setMaxWidth(Double.MAX_VALUE);
@ -598,15 +632,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
vBox.getChildren().addAll(headerLabel, descriptionLabel); vBox.getChildren().addAll(headerLabel, descriptionLabel);
add(vBox, 0, 0); add(vBox, 0, 0);
StackPane graphicContainer = new StackPane(); StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
graphicContainer.getStyleClass().add("graphic-container"); graphicContainer.getStyleClass().add("graphic-container");
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
graphicContainer.getChildren().add(imageView);
}
add(graphicContainer, 1, 0); add(graphicContainer, 1, 0);
ColumnConstraints textColumn = new ColumnConstraints(); ColumnConstraints textColumn = new ColumnConstraints();

View file

@ -5,6 +5,8 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -55,7 +57,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
super.updateItem(entry, empty); super.updateItem(entry, empty);
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442) //Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
if(this == lastCell && !getTableRow().isVisible()) { if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
return; return;
} }
lastCell = this; lastCell = this;
@ -66,8 +68,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setText(null); setText(null);
setGraphic(null); setGraphic(null);
} else { } else {
if(entry instanceof TransactionEntry) { if(entry instanceof TransactionEntry transactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
if(transactionEntry.getBlockTransaction().getHeight() == -1) { if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent"); setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry)); setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
@ -101,7 +102,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
actionBox.getChildren().add(viewTransactionButton); actionBox.getChildren().add(viewTransactionButton);
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) && if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
Button increaseFeeButton = new Button(""); Button increaseFeeButton = new Button("");
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph()); increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
@ -121,8 +122,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
setGraphic(actionBox); setGraphic(actionBox);
} else if(entry instanceof NodeEntry) { } else if(entry instanceof NodeEntry nodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress(); Address address = nodeEntry.getAddress();
setText(address.toString()); setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView())); setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
@ -163,8 +163,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
setContextMenu(null); setContextMenu(null);
setGraphic(new HBox()); setGraphic(new HBox());
} }
} else if(entry instanceof HashIndexEntry) { } else if(entry instanceof HashIndexEntry hashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription()); setText(hashIndexEntry.getDescription());
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry)); setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
Tooltip tooltip = new Tooltip(); Tooltip tooltip = new Tooltip();
@ -212,13 +211,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) { private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos(); Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream() List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry) .filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e) .map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable()) .filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex())) .map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled()) .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get()) .map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -243,6 +243,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
.collect(Collectors.toList()); .collect(Collectors.toList());
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1; boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum(); long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction(); Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize(); double vSize = tx.getVirtualSize();
@ -257,7 +258,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
Collections.shuffle(outputGroups); Collections.shuffle(outputGroups);
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) { while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
//If there is insufficient change output, include another random output group so the fee can be increased //If there is insufficient change output, include another random output group so the fee can be increased
OutputGroup outputGroup = outputGroups.remove(0); OutputGroup outputGroup = outputGroups.remove(0);
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
@ -298,9 +299,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
label += " (Replaced By Fee)"; label += " (Replaced By Fee)";
} }
if(txOutput.getScript().getToAddress() != null) { Address address = txOutput.getScript().getToAddress();
if(address != null) {
long value = txOutput.getValue();
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included //Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0); boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
} }
return null; return null;
@ -337,7 +342,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
} }
private static Double getMaxFeeRate() { private static Double getMaxFeeRate() {
@ -394,11 +399,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Payment payment = new Payment(freshAddress, label, inputTotal, true); Payment payment = new Payment(freshAddress, label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos)); EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null))); Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
} }
private static boolean canRBF(BlockTransaction blockTransaction) { private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee(); return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
} }
private static boolean canSignMessage(WalletNode walletNode) { private static boolean canSignMessage(WalletNode walletNode) {
@ -476,7 +481,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB"; tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
} }
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled"); tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
} }
return tooltip; return tooltip;
@ -544,6 +549,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static class UnconfirmedTransactionContextMenu extends ContextMenu { private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) { public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
Wallet wallet = transactionEntry.getWallet();
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
MenuItem viewTransaction = new MenuItem("View Transaction"); MenuItem viewTransaction = new MenuItem("View Transaction");
viewTransaction.setGraphic(getViewTransactionGlyph()); viewTransaction.setGraphic(getViewTransactionGlyph());
@ -553,7 +559,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}); });
getItems().add(viewTransaction); getItems().add(viewTransaction);
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)"); MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
increaseFee.setGraphic(getIncreaseFeeRBFGlyph()); increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
increaseFee.setOnAction(AE -> { increaseFee.setOnAction(AE -> {
@ -564,7 +570,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(increaseFee); getItems().add(increaseFee);
} }
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)"); MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
cancelTx.setGraphic(getCancelTransactionRBFGlyph()); cancelTx.setGraphic(getCancelTransactionRBFGlyph());
cancelTx.setOnAction(AE -> { cancelTx.setOnAction(AE -> {
@ -850,4 +856,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
} }
} }
} }
private boolean isTableSizeRecalculation() {
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
&& frame.getMethodName().equals("releaseCell")));
}
} }

View file

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

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.FileImport; import com.sparrowwallet.sparrow.io.FileImport;
@ -44,8 +45,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private final boolean fileFormatAvailable; private final boolean fileFormatAvailable;
protected List<Wallet> wallets; protected List<Wallet> wallets;
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) { public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
super(title, description, content, imageUrl); super(title, description, content, walletModel);
this.importer = importer; this.importer = importer;
this.scannable = scannable; this.scannable = scannable;
this.fileFormatAvailable = fileFormatAvailable; this.fileFormatAvailable = fileFormatAvailable;

View file

@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
private final boolean file; private final boolean file;
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) { public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png"); super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
this.keystore = keystore; this.keystore = keystore;
this.exporter = exporter; this.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable(); this.scannable = exporter.isKeystoreExportScannable();

View file

@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeyDerivation requiredDerivation; private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) { public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable()); super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet; this.wallet = wallet;
this.importer = importer; this.importer = importer;
this.requiredDerivation = requiredDerivation; this.requiredDerivation = requiredDerivation;

View file

@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private final boolean file; private final boolean file;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) { public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png"); super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
this.wallet = wallet; this.wallet = wallet;
this.exporter = exporter; this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable(); this.scannable = exporter.isWalletExportScannable();
@ -168,7 +168,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true); qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) { } else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false); qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Bip129) { } else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
UR ur = UR.fromBytes(outputStream.toByteArray()); UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray()); BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false); qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);

View file

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

View file

@ -42,7 +42,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
private String password; private String password;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) { public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable()); super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.importer = importer; this.importer = importer;
} }

View file

@ -38,12 +38,23 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
if(empty) { if(empty) {
setText(null); setText(null);
setGraphic(null); setGraphic(null);
setTooltip(null);
} else { } else {
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue(); Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
EntryCell.applyRowStyles(this, entry); EntryCell.applyRowStyles(this, entry);
setText(label); setText(label);
setContextMenu(new LabelContextMenu(entry, label)); setContextMenu(new LabelContextMenu(entry, label));
double width = label == null || label.length() < 20 ? 0.0 : TextUtils.computeTextWidth(getFont(), label, 0.0D);
if(width > getTableColumn().getWidth()) {
Tooltip tooltip = new Tooltip(label);
tooltip.setMaxWidth(getTreeTableView().getWidth());
tooltip.setWrapText(true);
setTooltip(tooltip);
} else {
setTooltip(null);
}
} }
} }
@ -121,7 +132,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
return confirmationsProperty; return confirmationsProperty;
} }
private static class LabelContextMenu extends ContextMenu { private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Entry entry, String label) { public LabelContextMenu(Entry entry, String label) {
MenuItem copyLabel = new MenuItem("Copy Label"); MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> { copyLabel.setOnAction(AE -> {
@ -141,6 +152,13 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
} }
}); });
getItems().add(pasteLabel); getItems().add(pasteLabel);
MenuItem editLabel = new MenuItem("Edit Label...");
editLabel.setOnAction(AE -> {
hide();
startEdit();
});
getItems().add(editLabel);
} }
} }
} }

View file

@ -118,14 +118,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title); dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
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 vBox = new VBox();
vBox.setSpacing(20); vBox.setSpacing(20);
@ -247,6 +240,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
setFormatFromScriptType(address.getScriptType()); setFormatFromScriptType(address.getScriptType());
if(wallet != null) { if(wallet != null) {
setWalletNodeFromAddress(wallet, address); setWalletNodeFromAddress(wallet, address);
if(walletNode != null) {
setFormatFromScriptType(getSigningScriptType(walletNode));
}
} }
} catch(InvalidAddressException e) { } catch(InvalidAddressException e) {
//can't happen //can't happen
@ -280,7 +276,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
if(wallet != null && walletNode != null) { if(wallet != null && walletNode != null) {
setFormatFromScriptType(wallet.getScriptType()); setFormatFromScriptType(getSigningScriptType(walletNode));
} else { } else {
formatGroup.selectToggle(formatElectrum); formatGroup.selectToggle(formatElectrum);
} }
@ -294,9 +290,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
private boolean canSign(Wallet wallet) { private boolean canSign(Wallet wallet) {
return wallet.getKeystores().get(0).hasPrivateKey() return wallet.getKeystores().getFirst().hasPrivateKey()
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|| wallet.getKeystores().get(0).getWalletModel().isCard(); || wallet.getKeystores().getFirst().getWalletModel().isCard();
}
private boolean canSignBip322(Wallet wallet) {
return wallet.getKeystores().getFirst().hasPrivateKey();
} }
private Address getAddress()throws InvalidAddressException { private Address getAddress()throws InvalidAddressException {
@ -320,6 +320,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
walletNode = wallet.getWalletAddresses().get(address); walletNode = wallet.getWalletAddresses().get(address);
} }
private ScriptType getSigningScriptType(WalletNode walletNode) {
ScriptType scriptType = walletNode.getWallet().getScriptType();
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
}
private void setFormatFromScriptType(ScriptType scriptType) { private void setFormatFromScriptType(ScriptType scriptType) {
formatElectrum.setDisable(scriptType == ScriptType.P2TR); formatElectrum.setDisable(scriptType == ScriptType.P2TR);
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH); formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
@ -352,7 +357,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
//Note we can expect a single keystore due to the check in the constructor //Note we can expect a single keystore due to the check in the constructor
Wallet signingWallet = walletNode.getWallet(); Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getKeystores().get(0).hasPrivateKey()) { if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
if(signingWallet.isEncrypted()) { if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent()); EventManager.get().post(new RequestOpenWalletsEvent());
} else { } else {
@ -365,7 +370,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void signUnencryptedKeystore(Wallet decryptedWallet) { private void signUnencryptedKeystore(Wallet decryptedWallet) {
try { try {
Keystore keystore = decryptedWallet.getKeystores().get(0); Keystore keystore = decryptedWallet.getKeystores().getFirst();
ECKey privKey = keystore.getKey(walletNode); ECKey privKey = keystore.getKey(walletNode);
String signatureText; String signatureText;
if(isBip322()) { if(isBip322()) {
@ -385,8 +390,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
private void signDeviceKeystore(Wallet deviceWallet) { private void signDeviceKeystore(Wallet deviceWallet) {
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation()); KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation); DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow()); deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait(); Optional<String> optSignature = deviceSignMessageDialog.showAndWait();

View file

@ -50,8 +50,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!"); dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
javafx.scene.image.Image image = new Image("/image/border-wallets.png"); dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
dialogPane.setGraphic(new ImageView(image));
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT]; String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
Grid grid = getGrid(emptyWordGrid); Grid grid = getGrid(emptyWordGrid);

View file

@ -19,7 +19,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
private final DeterministicSeed.Type type; private final DeterministicSeed.Type type;
public MnemonicKeystoreDisplayPane(Keystore keystore) { public MnemonicKeystoreDisplayPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png"); super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
showHideLink.setVisible(false); showHideLink.setVisible(false);
buttonBox.getChildren().clear(); buttonBox.getChildren().clear();
this.type = keystore.getSeed().getType(); this.type = keystore.getSeed().getType();

View file

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

View file

@ -45,7 +45,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private List<String> generatedMnemonicCode; private List<String> generatedMnemonicCode;
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) { public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet; this.wallet = wallet;
this.importer = importer; this.importer = importer;
this.defaultDerivation = defaultDerivation; this.defaultDerivation = defaultDerivation;

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode; import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed; import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode; import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
@ -51,8 +52,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty(""); protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
protected IntegerProperty defaultWordSizeProperty; protected IntegerProperty defaultWordSizeProperty;
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) { public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
super(title, description, content, imageUrl); super(title, description, content, walletModel);
} }
@Override @Override
@ -320,6 +321,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
} }
}; };
wordField.setMaxWidth(100); wordField.setMaxWidth(100);
wordField.setAccessibleText("Word " + (wordNumber + 1));
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> { TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
String text = change.getText(); String text = change.getText();
// if text was added, fix the text to fit the requirements // if text was added, fix the text to fit the requirements

View file

@ -44,7 +44,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
private int currentShare; private int currentShare;
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) { public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet; this.wallet = wallet;
this.importer = importer; this.importer = importer;
this.defaultDerivation = defaultDerivation; this.defaultDerivation = defaultDerivation;

View file

@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
private Button importButton; private Button importButton;
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) { public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.importer = importer; this.importer = importer;
} }

View file

@ -14,6 +14,7 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
@ -61,6 +62,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private final TextArea key; private final TextArea key;
private final ComboBox<ScriptType> keyScriptType; private final ComboBox<ScriptType> keyScriptType;
private final CopyableLabel keyAddress; private final CopyableLabel keyAddress;
private final CopyableLabel keyUtxos;
private final ComboBoxTextField toAddress; private final ComboBoxTextField toAddress;
private final ComboBox<Wallet> toWallet; private final ComboBox<Wallet> toWallet;
private final FeeRangeSlider feeRange; private final FeeRangeSlider feeRange;
@ -72,14 +74,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText("Sweep Private Key"); dialogPane.setHeaderText("Sweep Private Key");
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
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);
}
Form form = new Form(); Form form = new Form();
Fieldset fieldset = new Fieldset(); Fieldset fieldset = new Fieldset();
@ -136,6 +131,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
keyAddress.getStyleClass().add("fixed-width"); keyAddress.getStyleClass().add("fixed-width");
addressField.getInputs().add(keyAddress); addressField.getInputs().add(keyAddress);
Field utxosField = new Field();
utxosField.setText("UTXOs:");
keyUtxos = new CopyableLabel();
utxosField.getInputs().add(keyUtxos);
Field toAddressField = new Field(); Field toAddressField = new Field();
toAddressField.setText("Sweep to:"); toAddressField.setText("Sweep to:");
toAddress = new ComboBoxTextField(); toAddress = new ComboBoxTextField();
@ -355,6 +356,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
Optional<Date> optSince = addressScanDateDialog.showAndWait(); Optional<Date> optSince = addressScanDateDialog.showAndWait();
if(optSince.isPresent()) { if(optSince.isPresent()) {
since = optSince.get(); since = optSince.get();
} else {
return;
} }
} }
@ -369,7 +372,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}); });
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService); ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", new DialogImage(DialogImage.Type.SPARROW), addressUtxosService);
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow()); serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
AppServices.moveToActiveWindowScreen(serviceProgressDialog); AppServices.moveToActiveWindowScreen(serviceProgressDialog);
} }
@ -395,14 +398,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
double feeRate = feeRange.getFeeRate(); double feeRate = feeRange.getFeeRate();
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate); long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) { if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
fee++; fee++;
} }
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE); long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) { if(total - fee <= dustThreshold) {
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE; feeRate = AppServices.getMinimumRelayFeeRate();
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1; fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
if(total - fee <= dustThreshold) { if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats)."); AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");

View file

@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*; import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.*; import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKHAddress; import com.sparrowwallet.drongo.address.P2PKHAddress;
@ -27,7 +27,6 @@ import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent; import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder; import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
import com.sparrowwallet.sparrow.io.bbqr.BBQRException; import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
@ -39,14 +38,16 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders; import org.controlsfx.tools.Borders;
import org.openpnp.capture.CaptureDevice;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -76,108 +77,141 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)"); private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
private static final int SCAN_PERIOD_MILLIS = 100; private static final int SCAN_PERIOD_MILLIS = 100;
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA); private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.HD);
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0); private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>(); private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
private final ObservableList<WebcamResolution> availableResolutions = FXCollections.observableList(new ArrayList<>());
private boolean postOpenUpdate;
public QRScanDialog() { public QRScanDialog() {
this.urDecoder = new URDecoder(); this.urDecoder = new URDecoder();
this.legacyUrDecoder = new LegacyURDecoder(); this.legacyUrDecoder = new LegacyURDecoder();
this.bbqrDecoder = new BBQRDecoder(); this.bbqrDecoder = new BBQRDecoder();
if(Config.get().isHdCapture()) { if(Config.get().getWebcamResolution() != null) {
webcamResolutionProperty.set(WebcamResolution.HD); webcamResolutionProperty.set(Config.get().getWebcamResolution());
} }
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator()); this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS)); webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
webcamService.setRestartOnFailure(false); webcamService.setRestartOnFailure(false);
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
final DialogPane dialogPane = new QRScanDialogPane(); final DialogPane dialogPane = new QRScanDialogPane();
setDialogPane(dialogPane); setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane(); WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
stackPane.getChildren().add(webcamView.getView());
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
ProgressBar progressBar = new ProgressBar(); ProgressBar progressBar = new ProgressBar();
progressBar.setMinHeight(20); progressBar.setMinHeight(20);
progressBar.setPadding(new Insets(0, 10, 0, 10)); progressBar.setPadding(new Insets(0, 10, 0, 10));
progressBar.setPrefWidth(Integer.MAX_VALUE); progressBar.setPrefWidth(Integer.MAX_VALUE);
progressBar.progressProperty().bind(percentComplete); 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 vBox = new VBox(20);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
vBox.getChildren().addAll(wrappedView, progressBar); vBox.getChildren().addAll(wrappedView, progressBar);
dialogPane.setContent(vBox); dialogPane.setContent(vBox);
webcamService.openingProperty().addListener((_, _, opening) -> {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
}
});
webcamService.openedProperty().addListener((_, _, opened) -> {
if(opened) {
Platform.runLater(() -> {
try {
postOpenUpdate = true;
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
newDevices.removeAll(foundDevices);
foundDevices.addAll(newDevices);
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
if(webcamService.getDevice() != null) {
for(CaptureDevice device : foundDevices) {
if(device.equals(webcamService.getDevice())) {
webcamDeviceProperty.set(device);
}
}
}
updateList(availableResolutions, webcamService.getResolutions());
webcamResolutionProperty.set(webcamService.getResolution());
} finally {
postOpenUpdate = false;
}
});
} else if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
});
webcamService.resultProperty().addListener(new QRResultListener()); webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> { webcamService.setOnFailed(failedEvent -> {
Throwable exception = failedEvent.getSource().getException(); Throwable exception = Throwables.getRootCause(failedEvent.getSource().getException());
Platform.runLater(() -> setResult(new Result(exception)));
Throwable nested = exception;
while(nested.getCause() != null) {
nested = nested.getCause();
}
if(OsType.getCurrent() == OsType.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(); webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
webcamResolutionProperty.addListener((_, oldResolution, newResolution) -> {
if(newResolution != null) { if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100)); if(newResolution.isStandardAspect() && oldResolution.isWidescreenAspect()) {
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD)); setWidth(getWidth());
setHeight(getHeight() + 100);
dialogPane.setMaxHeight(dialogPane.getPrefHeight() + 100);
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
dialogPane.setMinHeight(dialogPane.getMaxHeight());
} else if(newResolution.isWidescreenAspect() && oldResolution.isStandardAspect()) {
setWidth(getWidth());
setHeight(getHeight() - 100);
dialogPane.setMaxHeight(dialogPane.getPrefHeight() - 100);
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
dialogPane.setMinHeight(dialogPane.getMaxHeight());
}
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution));
}
if(newResolution == null || !postOpenUpdate) {
webcamService.cancel();
} }
webcamService.cancel();
}); });
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> { webcamDeviceProperty.addListener((_, _, newValue) -> {
Config.get().setWebcamDevice(newValue.getName()); Config.get().setWebcamDevice(newValue.getName());
Config.get().setWebcamDeviceId(newValue.getUniqueId());
if(!Objects.equals(webcamService.getDevice(), newValue)) { if(!Objects.equals(webcamService.getDevice(), newValue)) {
webcamService.cancel(); webcamService.cancel();
} }
}); });
setOnCloseRequest(event -> { setOnCloseRequest(_ -> {
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD); if(webcamResolutionProperty.get() != null) {
if(Config.get().isHdCapture() != isHdCapture) { Config.get().setWebcamResolution(webcamResolutionProperty.get());
Config.get().setHdCapture(isHdCapture);
} }
Platform.runLater(() -> webcamResolutionProperty.set(null)); Platform.runLater(() -> {
webcamResolutionProperty.set(null);
webcamService.close();
});
}); });
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE); final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT); final ButtonType deviceButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.LEFT);
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2); final ButtonType resolutionButtonType = new javafx.scene.control.ButtonType("Resolution", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType); dialogPane.getButtonTypes().addAll(deviceButtonType, resolutionButtonType, cancelButtonType);
dialogPane.setPrefWidth(646); dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590); dialogPane.setPrefHeight(webcamResolutionProperty.get().isWidescreenAspect() ? 490 : 590);
dialogPane.setMinHeight(dialogPane.getPrefHeight()); dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this); AppServices.moveToActiveWindowScreen(this);
@ -685,72 +719,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
} }
} }
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 { private class QRScanDialogPane extends DialogPane {
@Override @Override
protected Node createButton(ButtonType buttonType) { protected Node createButton(ButtonType buttonType) {
Node button = null; Node button;
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton hd = new ToggleButton(buttonType.getText()); ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(foundDevices);
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<>() { devicesCombo.setConverter(new StringConverter<>() {
@Override @Override
public String toString(WebcamDevice device) { public String toString(CaptureDevice device) {
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera"; return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
} }
@Override @Override
public WebcamDevice fromString(String string) { public CaptureDevice fromString(String string) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
}); });
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty); devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT); final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(devicesCombo, buttonData);
button = devicesCombo; button = devicesCombo;
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
ComboBox<WebcamResolution> resolutionsCombo = new ComboBox<>(availableResolutions);
resolutionsCombo.valueProperty().bindBidirectional(webcamResolutionProperty);
ButtonBar.setButtonData(resolutionsCombo, ButtonBar.ButtonData.LEFT);
button = resolutionsCombo;
} else { } else {
button = super.createButton(buttonType); button = super.createButton(buttonType);
} }
@ -763,19 +757,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
button.disableProperty().bind(webcamService.openingProperty()); button.disableProperty().bind(webcamService.openingProperty());
return button; return button;
} }
}
private void setHdGraphic(ToggleButton hd, boolean isHd) { public static <T extends Comparable<T>> void updateList(List<T> targetList, Collection<T> sourceList) {
if(isHd) { List<T> sortedSource = new ArrayList<>(sourceList);
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE)); Collections.sort(sortedSource);
ListIterator<T> targetIter = targetList.listIterator();
int sourceIndex = 0;
while (sourceIndex < sortedSource.size() && targetIter.hasNext()) {
T sourceItem = sortedSource.get(sourceIndex);
T targetItem = targetIter.next();
int comparison = sourceItem.compareTo(targetItem);
if (comparison < 0) {
targetIter.previous(); // Back up to insert before
targetIter.add(sourceItem);
sourceIndex++;
} else if (comparison > 0) {
targetIter.remove();
} else { } else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN)); sourceIndex++;
} }
} }
private Glyph getGlyph(FontAwesome5.Glyph glyphName) { while (sourceIndex < sortedSource.size()) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName); targetIter.add(sortedSource.get(sourceIndex));
glyph.setFontSize(11); sourceIndex++;
return glyph; }
while (targetIter.hasNext()) {
targetIter.next();
targetIter.remove();
} }
} }
@ -993,10 +1007,4 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
super(message, 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);
}
}
} }

View file

@ -0,0 +1,178 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.BlockSummary;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import javafx.animation.TranslateTransition;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
public class RecentBlocksView extends Pane {
private static final double CUBE_SPACING = 100;
private static final double ANIMATION_DURATION_MILLIS = 1000;
private static final double SEPARATOR_X = 74;
private final CompositeDisposable disposables = new CompositeDisposable();
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
private final Tooltip tooltip = new Tooltip();
public RecentBlocksView() {
cubesProperty.addListener((_, _, newValue) -> {
if(newValue != null && newValue.size() == 3) {
drawView();
}
});
Rectangle clip = new Rectangle(-20, -40, CUBE_SPACING * 3 - 20, 100);
setClip(clip);
Observable<Long> intervalObservable = Observable.interval(1, TimeUnit.MINUTES);
disposables.add(intervalObservable.observeOn(JavaFxScheduler.platform()).subscribe(_ -> {
for(BlockCube cube : getCubes()) {
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
}
}));
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
updateFeeRatesSource(feeRatesSource);
Tooltip.install(this, tooltip);
}
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
if(getCubes() != null && !getCubes().isEmpty()) {
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
}
}
public void drawView() {
createSeparator();
for(int i = 0; i < 3; i++) {
BlockCube cube = getCubes().get(i);
cube.setTranslateX(i * CUBE_SPACING);
getChildren().add(cube);
}
}
private void createSeparator() {
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE);
separator.getStyleClass().add("blocks-separator");
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
separator.setStrokeWidth(1.0);
getChildren().add(separator);
}
public void update(List<BlockSummary> latestBlocks, Double currentFeeRate) {
if(getCubes().isEmpty()) {
List<BlockCube> cubes = new ArrayList<>();
cubes.add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
cubes.addAll(latestBlocks.stream().map(BlockCube::fromBlockSummary).limit(2).toList());
setCubes(cubes);
} else {
int knownTip = getCubes().stream().mapToInt(BlockCube::getHeight).max().orElse(0);
int latestTip = latestBlocks.stream().mapToInt(BlockSummary::getHeight).max().orElse(0);
if(latestTip > knownTip) {
addNewBlock(latestBlocks, currentFeeRate);
} else {
for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) {
BlockCube blockCube = getCubes().get(i);
BlockSummary latestBlock = latestBlocks.get(i - 1);
blockCube.setConfirmed(true);
blockCube.setHeight(latestBlock.getHeight());
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
blockCube.setWeight(latestBlock.getWeight().orElse(0));
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
}
updateFeeRate(currentFeeRate);
}
}
}
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
if(getCubes().isEmpty()) {
return;
}
for(int i = 0; i < getCubes().size() && i < latestBlocks.size(); i++) {
BlockCube blockCube = getCubes().get(i);
BlockSummary latestBlock = latestBlocks.get(i);
blockCube.setConfirmed(true);
blockCube.setHeight(latestBlock.getHeight());
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
blockCube.setWeight(latestBlock.getWeight().orElse(0));
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
}
add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
}
public void add(BlockCube newCube) {
newCube.setTranslateX(-CUBE_SPACING);
getChildren().add(newCube);
getCubes().getFirst().setConfirmed(true);
getCubes().addFirst(newCube);
animateCubes();
if(getCubes().size() > 4) {
BlockCube lastCube = getCubes().getLast();
getChildren().remove(lastCube);
getCubes().remove(lastCube);
}
}
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
if(targetBlockFeeRates.get(defaultTarget) != null) {
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
updateFeeRate(defaultRate);
}
}
public void updateFeeRate(Double currentFeeRate) {
if(!getCubes().isEmpty()) {
BlockCube firstCube = getCubes().getFirst();
firstCube.setMedianFee(currentFeeRate);
}
}
private void animateCubes() {
for(int i = 0; i < getCubes().size(); i++) {
BlockCube cube = getCubes().get(i);
TranslateTransition transition = new TranslateTransition(Duration.millis(ANIMATION_DURATION_MILLIS), cube);
transition.setToX(i * CUBE_SPACING);
transition.play();
}
}
public List<BlockCube> getCubes() {
return cubesProperty.get();
}
public ObjectProperty<List<BlockCube>> cubesProperty() {
return cubesProperty;
}
public void setCubes(List<BlockCube> cubes) {
this.cubesProperty.set(cubes);
}
}

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk; import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator; import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration; import org.controlsfx.control.decoration.GraphicDecoration;
@ -53,7 +54,11 @@ public class ScriptArea extends CodeArea {
for (int i = 0; i < script.getChunks().size(); i++) { for (int i = 0; i < script.getChunks().size(); i++) {
ScriptChunk chunk = script.getChunks().get(i); ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) { if(chunk.isOpCode()) {
append(chunk.toString(), "script-opcode"); if(chunk.getOpcode() == ScriptOpCodes.OP_0 && witnessScript != null) {
append("<empty>", "script-other");
} else {
append(chunk.toString(), "script-opcode");
}
} else if(chunk.isPubKey()) { } else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey"); append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isSignature()) { } else if(chunk.isSignature()) {

View file

@ -60,14 +60,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName()); dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if(!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox(); VBox vBox = new VBox();
vBox.setSpacing(20); vBox.setSpacing(20);

View file

@ -5,36 +5,49 @@ import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.Config;
import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.controlsfx.control.spreadsheet.*; import org.controlsfx.control.spreadsheet.*;
import org.controlsfx.glyphfont.Glyph;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> { public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit; private final BitcoinUnit bitcoinUnit;
private final SpreadsheetView spreadsheetView; private final SpreadsheetView spreadsheetView;
public static final AddressCellType ADDRESS = new AddressCellType(); public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit) { public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
this.bitcoinUnit = bitcoinUnit; this.bitcoinUnit = bitcoinUnit;
final DialogPane dialogPane = new SendToManyDialogPane(); final DialogPane dialogPane = new SendToManyDialogPane();
@ -42,10 +55,10 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
setTitle("Send to Many"); setTitle("Send to Many");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary."); dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
Image image = new Image("/image/sparrow-small.png"); dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
dialogPane.setGraphic(new ImageView(image));
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList()); List<Payment> initialPayments = IntStream.range(0, 100)
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
Grid grid = getGrid(initialPayments); Grid grid = getGrid(initialPayments);
spreadsheetView = new SpreadsheetView(grid) { spreadsheetView = new SpreadsheetView(grid) {
@ -70,14 +83,16 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
dialogPane.setContent(stackPane); dialogPane.setContent(stackPane);
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
okButton.addEventFilter(ActionEvent.ACTION, event -> {
getPayments();
event.consume();
});
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT); final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(loadCsvButtonType); dialogPane.getButtonTypes().add(loadCsvButtonType);
setResultConverter((dialogButton) -> { setResultConverter((_) -> null);
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
});
dialogPane.setPrefWidth(850); dialogPane.setPrefWidth(850);
dialogPane.setPrefHeight(500); dialogPane.setPrefHeight(500);
@ -87,18 +102,24 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
private Grid getGrid(List<Payment> payments) { private Grid getGrid(List<Payment> payments) {
int rowCount = payments.size(); return createGrid(payments.stream().map(payment -> new SendToPayment(payment, SendToAddress.fromPayment(payment))).collect(Collectors.toList()));
}
private Grid createGrid(List<SendToPayment> sendToPayments) {
int rowCount = sendToPayments.size();
int columnCount = 3; int columnCount = 3;
GridBase grid = new GridBase(rowCount, columnCount); GridBase grid = new GridBase(rowCount, columnCount);
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList(); ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
for(int row = 0; row < grid.getRowCount(); ++row) { for(int row = 0; row < grid.getRowCount(); ++row) {
SendToPayment sendToPayment = sendToPayments.get(row);
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList(); final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress()); SendToAddress sendToAddress = sendToPayment.sendToAddress();
SpreadsheetCell addressCell = SEND_TO_ADDRESS.createCell(row, 0, 1, 1, sendToAddress);
addressCell.getStyleClass().add("fixed-width"); addressCell.getStyleClass().add("fixed-width");
list.add(addressCell); list.add(addressCell);
double amount = (double)payments.get(row).getAmount(); double amount = (double)sendToPayment.payment().getAmount();
if(bitcoinUnit == BitcoinUnit.BTC) { if(bitcoinUnit == BitcoinUnit.BTC) {
amount = amount / Transaction.SATOSHIS_PER_BITCOIN; amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
} }
@ -110,7 +131,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
list.add(amountCell); list.add(amountCell);
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel())); list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, sendToPayment.payment().getLabel()));
rows.add(list); rows.add(list);
} }
grid.setRows(rows); grid.setRows(rows);
@ -119,32 +140,49 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return grid; return grid;
} }
private List<Payment> getPayments() { private void getPayments() {
List<Payment> payments = new ArrayList<>(); if(needsResolution() && Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
Grid grid = spreadsheetView.getGrid(); if(Config.get().getConnectToResolve() == null || Config.get().getConnectToResolve() == Boolean.FALSE) {
String firstLabel = null; Platform.runLater(() -> {
for(int row = 0; row < grid.getRowCount(); row++) { ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to resolve?", "You are currently offline. Connect to resolve the addresses?", ButtonType.NO, ButtonType.YES);
Optional<ButtonType> optType = confirmationAlert.showAndWait();
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
Config.get().setConnectToResolve(optType.get() == ButtonType.YES);
}
if(optType.isPresent() && optType.get() == ButtonType.YES) {
EventManager.get().post(new RequestConnectEvent());
}
});
} else {
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
}
return;
}
CreatePaymentsService createPaymentsService = new CreatePaymentsService();
createPaymentsService.setOnSucceeded(_ -> {
List<Payment> payments = createPaymentsService.getValue();
if(payments != null) {
setResult(payments);
}
});
createPaymentsService.setOnFailed(event -> {
Throwable ex = event.getSource().getException();
AppServices.showErrorDialog("Error creating payments", ex.getMessage());
});
createPaymentsService.start();
}
private boolean needsResolution() {
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row); ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
Address address = (Address)rowCells.get(0).getItem(); SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
Double value = (Double)rowCells.get(1).getItem(); if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
String label = (String)rowCells.get(2).getItem(); return true;
if(firstLabel == null) {
firstLabel = label;
}
if(label == null || label.isEmpty()) {
label = firstLabel;
}
if(address != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
payments.add(new Payment(address, label, value.longValue(), false));
} }
} }
return payments; return false;
} }
private class SendToManyDialogPane extends DialogPane { private class SendToManyDialogPane extends DialogPane {
@ -154,7 +192,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) { if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
Button loadButton = new Button(buttonType.getText()); Button loadButton = new Button(buttonType.getText());
loadButton.setGraphicTextGap(5); loadButton.setGraphicTextGap(5);
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP)); loadButton.setGraphic(GlyphUtils.getUpArrowGlyph());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData(); final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(loadButton, buttonData); ButtonBar.setButtonData(loadButton, buttonData);
loadButton.setOnAction(event -> { loadButton.setOnAction(event -> {
@ -169,7 +207,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
File file = fileChooser.showOpenDialog(this.getScene().getWindow()); File file = fileChooser.showOpenDialog(this.getScene().getWindow());
if(file != null) { if(file != null) {
try { try {
List<Payment> csvPayments = new ArrayList<>(); List<SendToPayment> csvPayments = new ArrayList<>();
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) { try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
CsvReader csvReader = new CsvReader(reader); CsvReader csvReader = new CsvReader(reader);
while(csvReader.readRecord()) { while(csvReader.readRecord()) {
@ -185,9 +223,22 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} else { } else {
amount = Long.parseLong(csvReader.get(1).replace(",", "")); amount = Long.parseLong(csvReader.get(1).replace(",", ""));
} }
Address address = Address.fromString(csvReader.get(0));
String label = csvReader.get(2); String label = csvReader.get(2);
csvPayments.add(new Payment(address, label, amount, false)); Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
if(optDnsPaymentHrn.isPresent()) {
Payment payment = new Payment(null, label, amount, false);
csvPayments.add(new SendToPayment(payment, new SendToAddress(optDnsPaymentHrn.get())));
} else {
try {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(csvReader.get(0));
Payment payment = new SilentPayment(silentPaymentAddress, label, amount, false);
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
} catch(Exception e) {
Address address = Address.fromString(csvReader.get(0));
Payment payment = new Payment(address, label, amount, false);
csvPayments.add(new SendToPayment(payment, SendToAddress.fromPayment(payment)));
}
}
} catch(NumberFormatException e) { } catch(NumberFormatException e) {
//ignore and continue - probably a header line //ignore and continue - probably a header line
} catch(InvalidAddressException e) { } catch(InvalidAddressException e) {
@ -200,7 +251,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return; return;
} }
spreadsheetView.setGrid(getGrid(csvPayments)); spreadsheetView.setGrid(createGrid(csvPayments));
} }
} catch(IOException e) { } catch(IOException e) {
AppServices.showErrorDialog("Cannot load CSV", e.getMessage()); AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
@ -215,24 +266,18 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
return button; return button;
} }
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
} }
public static class AddressCellType extends SpreadsheetCellType<Address> { public static class SendToAddressCellType extends SpreadsheetCellType<SendToAddress> {
public AddressCellType() { public SendToAddressCellType() {
this(new StringConverterWithFormat<>(new AddressStringConverter()) { this(new StringConverterWithFormat<>(new SendToAddressStringConverter()) {
@Override @Override
public String toString(Address item) { public String toString(SendToAddress item) {
return toStringFormat(item, ""); //$NON-NLS-1$ return toStringFormat(item, ""); //$NON-NLS-1$
} }
@Override @Override
public Address fromString(String str) { public SendToAddress fromString(String str) {
if(str == null || str.isEmpty()) { //$NON-NLS-1$ if(str == null || str.isEmpty()) { //$NON-NLS-1$
return null; return null;
} else { } else {
@ -241,7 +286,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toStringFormat(Address item, String format) { public String toStringFormat(SendToAddress item, String format) {
try { try {
if(item == null) { if(item == null) {
return ""; //$NON-NLS-1$ return ""; //$NON-NLS-1$
@ -255,7 +300,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}); });
} }
public AddressCellType(StringConverter<Address> converter) { public SendToAddressCellType(StringConverter<SendToAddress> converter) {
super(converter); super(converter);
} }
@ -265,7 +310,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan, public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
final Address value) { final SendToAddress value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this); SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value); cell.setItem(value);
return cell; return cell;
@ -278,7 +323,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
@Override @Override
public boolean match(Object value, Object... options) { public boolean match(Object value, Object... options) {
if(value instanceof Address) if(value instanceof SendToAddress)
return true; return true;
else { else {
try { try {
@ -291,9 +336,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public Address convertValue(Object value) { public SendToAddress convertValue(Object value) {
if(value instanceof Address) if(value instanceof SendToAddress)
return (Address)value; return (SendToAddress)value;
else { else {
try { try {
return converter.fromString(value == null ? null : value.toString()); return converter.fromString(value == null ? null : value.toString());
@ -304,13 +349,155 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
} }
@Override @Override
public String toString(Address item) { public String toString(SendToAddress item) {
return converter.toString(item); return converter.toString(item);
} }
@Override @Override
public String toString(Address item, String format) { public String toString(SendToAddress item, String format) {
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format); return ((StringConverterWithFormat<SendToAddress>)converter).toStringFormat(item, format);
} }
}; };
public static class SendToAddress {
private final String hrn;
private final Address address;
private final SilentPaymentAddress silentPaymentAddress;
public SendToAddress(String hrn) {
this.hrn = hrn;
this.address = null;
this.silentPaymentAddress = null;
}
public SendToAddress(Address address) {
this.hrn = null;
this.address = address;
this.silentPaymentAddress = null;
}
public SendToAddress(SilentPaymentAddress silentPaymentAddress) {
this.hrn = null;
this.address = null;
this.silentPaymentAddress = silentPaymentAddress;
}
public String toString() {
return hrn == null ? silentPaymentAddress == null ? (address == null ? null : address.toString()) : silentPaymentAddress.toString() : hrn;
}
public static SendToAddress fromPayment(Payment payment) {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
if(dnsPayment != null) {
return new SendToAddress(dnsPayment.hrn());
}
return payment instanceof SilentPayment ? new SendToAddress(((SilentPayment)payment).getSilentPaymentAddress()) : new SendToAddress(payment.getAddress());
}
public Payment toPayment(String label, long value, boolean sendMax) throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
if(hrn != null) {
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(hrn);
if(dnsPayment == null) {
DnsPaymentResolver resolver = new DnsPaymentResolver(hrn);
Optional<DnsPayment> optDnsPayment = resolver.resolve();
if(optDnsPayment.isPresent()) {
dnsPayment = optDnsPayment.get();
if(dnsPayment.hasAddress()) {
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getAddress(), dnsPayment);
} else if(dnsPayment.hasSilentPaymentAddress()) {
DnsPaymentCache.putDnsPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), dnsPayment);
}
return getPayment(optDnsPayment.get(), label, value, sendMax);
} else {
throw new IllegalArgumentException("Payment to " + hrn + " could not be resolved.");
}
} else {
return getPayment(dnsPayment, label, value, sendMax);
}
}
if(silentPaymentAddress != null) {
return new SilentPayment(silentPaymentAddress, label, value, sendMax);
} else {
return new Payment(address, label, value, sendMax);
}
}
private static Payment getPayment(DnsPayment dnsPayment, String label, long value, boolean sendMax) {
if(dnsPayment.hasAddress()) {
return new Payment(dnsPayment.bitcoinURI().getAddress(), label, value, sendMax);
} else if(dnsPayment.hasSilentPaymentAddress()) {
return new SilentPayment(dnsPayment.bitcoinURI().getSilentPaymentAddress(), label, value, sendMax);
} else {
throw new IllegalArgumentException("Payment to " + dnsPayment + " has no associated address.");
}
}
}
private static class SendToAddressStringConverter extends StringConverter<SendToAddress> {
private final AddressStringConverter addressStringConverter = new AddressStringConverter();
@Override
public SendToAddress fromString(String value) {
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(value);
if(optDnsPaymentHrn.isPresent()) {
return new SendToAddress(optDnsPaymentHrn.get());
}
try {
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.from(value);
return new SendToAddress(silentPaymentAddress);
} catch(Exception e) {
Address address = addressStringConverter.fromString(value);
return address == null ? null : new SendToAddress(address);
}
}
@Override
public String toString(SendToAddress value) {
return value.toString();
}
}
private class CreatePaymentsService extends Service<List<Payment>> {
@Override
protected Task<List<Payment>> createTask() {
return new Task<>() {
@Override
protected List<Payment> call() throws Exception {
return getPayments();
}
};
}
private List<Payment> getPayments() throws DnsPaymentValidationException, IOException, ExecutionException, InterruptedException, BitcoinURIParseException {
List<Payment> payments = new ArrayList<>();
Grid grid = spreadsheetView.getGrid();
String firstLabel = null;
for(int row = 0; row < grid.getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
SendToAddress sendToAddress = (SendToAddress)rowCells.get(0).getItem();
Double value = (Double)rowCells.get(1).getItem();
String label = (String)rowCells.get(2).getItem();
if(firstLabel == null) {
firstLabel = label;
}
if(label == null || label.isEmpty()) {
label = firstLabel;
}
if(sendToAddress != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
}
}
return payments;
}
}
private record SendToPayment(Payment payment, SendToAddress sendToAddress) {}
} }

View file

@ -3,13 +3,12 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.concurrent.Worker; import javafx.concurrent.Worker;
import javafx.scene.Node;
import javafx.scene.control.DialogPane; import javafx.scene.control.DialogPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.controlsfx.dialog.ProgressDialog; import org.controlsfx.dialog.ProgressDialog;
public class ServiceProgressDialog extends ProgressDialog { public class ServiceProgressDialog extends ProgressDialog {
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) { public ServiceProgressDialog(String title, String header, Node graphic, Worker<?> worker) {
super(worker); super(worker);
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
@ -20,8 +19,7 @@ public class ServiceProgressDialog extends ProgressDialog {
setHeaderText(header); setHeaderText(header);
dialogPane.getStyleClass().remove("progress-dialog"); dialogPane.getStyleClass().remove("progress-dialog");
Image image = new Image(imagePath); dialogPane.setGraphic(graphic);
dialogPane.setGraphic(new ImageView(image));
} }
public static class ProxyWorker implements Worker<Boolean> { public static class ProxyWorker implements Worker<Boolean> {

View file

@ -44,8 +44,7 @@ public class TextAreaDialog extends Dialog<String> {
final DialogPane dialogPane = new TextAreaDialogPane(); final DialogPane dialogPane = new TextAreaDialogPane();
setDialogPane(dialogPane); setDialogPane(dialogPane);
Image image = new Image("/image/sparrow-small.png"); dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
dialogPane.setGraphic(new ImageView(image));
HBox hbox = new HBox(); HBox hbox = new HBox();
this.textArea = new TextArea(defaultValue); this.textArea = new TextArea(defaultValue);

View file

@ -29,8 +29,7 @@ public class TextfieldDialog extends Dialog<String> {
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
setDialogPane(dialogPane); setDialogPane(dialogPane);
Image image = new Image("/image/sparrow-small.png"); dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
dialogPane.setGraphic(new ImageView(image));
HBox hbox = new HBox(); HBox hbox = new HBox();
this.textField = new TextField(defaultValue); this.textField = new TextField(defaultValue);

View file

@ -2,14 +2,13 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -23,17 +22,18 @@ public class TitledDescriptionPane extends TitledPane {
protected Hyperlink showHideLink; protected Hyperlink showHideLink;
protected HBox buttonBox; protected HBox buttonBox;
public TitledDescriptionPane(String title, String description, String content, String imageUrl) { public TitledDescriptionPane(String title, String description, String content, WalletModel walletModel) {
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
getStyleClass().add("titled-description-pane"); getStyleClass().add("titled-description-pane");
setAccessibleText(title);
setPadding(Insets.EMPTY); setPadding(Insets.EMPTY);
setGraphic(getTitle(title, description, imageUrl)); setGraphic(getTitle(title, description, walletModel));
setContent(getContentBox(content)); setContent(getContentBox(content));
removeArrow(); removeArrow();
} }
protected Node getTitle(String title, String description, String imageUrl) { protected Node getTitle(String title, String description, WalletModel walletModel) {
HBox listItem = new HBox(); HBox listItem = new HBox();
listItem.setPadding(new Insets(10, 20, 10, 10)); listItem.setPadding(new Insets(10, 20, 10, 10));
listItem.setSpacing(10); listItem.setSpacing(10);
@ -43,12 +43,8 @@ public class TitledDescriptionPane extends TitledPane {
imageBox.setMinHeight(50); imageBox.setMinHeight(50);
listItem.getChildren().add(imageBox); listItem.getChildren().add(imageBox);
Image image = new Image(imageUrl, 50, 50, true, true); WalletModelImage walletModelImage = new WalletModelImage(walletModel);
if (!image.isError()) { imageBox.getChildren().add(walletModelImage);
ImageView imageView = new ImageView();
imageView.setImage(image);
imageBox.getChildren().add(imageView);
}
VBox labelsBox = new VBox(); VBox labelsBox = new VBox();
labelsBox.setSpacing(5); labelsBox.setSpacing(5);

View file

@ -3,19 +3,22 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent; import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent; import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy; import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@ -23,6 +26,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Group; import javafx.scene.Group;
@ -39,10 +43,7 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve; import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line; import javafx.scene.shape.Line;
import javafx.stage.FileChooser; import javafx.stage.*;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration; import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
@ -107,6 +108,7 @@ public class TransactionDiagram extends GridPane {
expandedDiagram.setId("transactionDiagram"); expandedDiagram.setId("transactionDiagram");
expandedDiagram.setExpanded(true); expandedDiagram.setExpanded(true);
expandedDiagram.setFinal(isFinal()); expandedDiagram.setFinal(isFinal());
expandedDiagram.setMaxWidth(AppServices.getActiveWindow().getWidth() - 200);
updateDerivedDiagram(expandedDiagram); updateDerivedDiagram(expandedDiagram);
HBox buttonBox = new HBox(); HBox buttonBox = new HBox();
@ -124,7 +126,7 @@ public class TransactionDiagram extends GridPane {
AppServices.setStageIcon(stage); AppServices.setStageIcon(stage);
stage.setScene(scene); stage.setScene(scene);
stage.setOnShowing(e -> { stage.setOnShowing(e -> {
AppServices.moveToActiveWindowScreen(stage, 600, 460); AppServices.moveToActiveWindowScreen(stage, expandedDiagram.getMaxWidth(), 460);
}); });
stage.setOnHidden(e -> { stage.setOnHidden(e -> {
expandedDiagram = null; expandedDiagram = null;
@ -141,6 +143,39 @@ public class TransactionDiagram extends GridPane {
} }
}; };
public TransactionDiagram() {
ColumnConstraints col1 = new ColumnConstraints();
col1.setPrefWidth(22);
col1.setHgrow(Priority.NEVER);
ColumnConstraints col2 = new ColumnConstraints();
col2.setHgrow(Priority.ALWAYS);
col2.setPercentWidth(25);
col2.setFillWidth(true);
ColumnConstraints col3 = new ColumnConstraints();
col3.setPrefWidth(140);
col3.setHgrow(Priority.NEVER);
ColumnConstraints col4 = new ColumnConstraints();
Label label = new Label();
col4.setMinWidth(TextUtils.computeTextWidth(label.getFont(), "Transaction", 0) + 20);
col4.setHgrow(Priority.NEVER);
col4.setHalignment(HPos.CENTER);
ColumnConstraints col5 = new ColumnConstraints();
col5.setPrefWidth(140);
col5.setHgrow(Priority.NEVER);
ColumnConstraints col6 = new ColumnConstraints();
col6.setHgrow(Priority.ALWAYS);
col6.setPercentWidth(25);
col6.setFillWidth(true);
getColumnConstraints().addAll(col1, col2, col3, col4, col5, col6);
setPadding(new Insets(0, 0, 0, 40));
}
public void update(WalletTransaction walletTx) { public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight()); setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight()); setMaxHeight(getDiagramHeight());
@ -169,7 +204,7 @@ public class TransactionDiagram extends GridPane {
VBox messagePane = new VBox(); VBox messagePane = new VBox();
messagePane.setPrefHeight(getDiagramHeight()); messagePane.setPrefHeight(getDiagramHeight());
messagePane.setPadding(new Insets(0, 10, 0, 280)); messagePane.setPadding(new Insets(0, 10, 0, 10));
messagePane.setAlignment(Pos.CENTER); messagePane.setAlignment(Pos.CENTER);
messagePane.getChildren().add(createSpacer()); messagePane.getChildren().add(createSpacer());
@ -229,6 +264,14 @@ public class TransactionDiagram extends GridPane {
GridPane.setConstraints(outputsPane, 5, 0); GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear(); getChildren().clear();
List<Payment> userPayments = getUserPayments();
if(!isFinal() && userPayments.size() > 1) {
Pane totalsPane = getTotalsPane(userPayments);
GridPane.setConstraints(totalsPane, 2, 0, 3, 1);
getChildren().add(totalsPane);
}
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
if(contextMenu == null) { if(contextMenu == null) {
@ -404,8 +447,6 @@ public class TransactionDiagram extends GridPane {
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) { private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
VBox inputsBox = new VBox(); VBox inputsBox = new VBox();
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
inputsBox.setPadding(new Insets(0, 10, 0, 10)); inputsBox.setPadding(new Insets(0, 10, 0, 10));
inputsBox.minHeightProperty().bind(minHeightProperty()); inputsBox.minHeightProperty().bind(minHeightProperty());
inputsBox.setAlignment(Pos.BASELINE_RIGHT); inputsBox.setAlignment(Pos.BASELINE_RIGHT);
@ -490,6 +531,11 @@ public class TransactionDiagram extends GridPane {
} }
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.setShowDuration(Duration.INDEFINITE); tooltip.setShowDuration(Duration.INDEFINITE);
tooltip.setWrapText(true);
Window activeWindow = AppServices.getActiveWindow();
if(activeWindow != null) {
tooltip.setMaxWidth(activeWindow.getWidth());
}
if(!tooltip.getText().isEmpty()) { if(!tooltip.getText().isEmpty()) {
label.setTooltip(tooltip); label.setTooltip(tooltip);
} }
@ -613,6 +659,10 @@ public class TransactionDiagram extends GridPane {
} }
} }
private List<Payment> getUserPayments() {
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
}
private Pane getOutputsLines(List<Payment> displayedPayments) { private Pane getOutputsLines(List<Payment> displayedPayments) {
VBox pane = new VBox(); VBox pane = new VBox();
Group group = new Group(); Group group = new Group();
@ -628,7 +678,8 @@ public class TransactionDiagram extends GridPane {
double width = 140.0; double width = 140.0;
long sum = walletTx.getTotal(); long sum = walletTx.getTotal();
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList()); List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee()); values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1; int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) { for(int i = 1; i <= numOutputs; i++) {
@ -664,8 +715,6 @@ public class TransactionDiagram extends GridPane {
private Pane getOutputsLabels(List<Payment> displayedPayments) { private Pane getOutputsLabels(List<Payment> displayedPayments) {
VBox outputsBox = new VBox(); VBox outputsBox = new VBox();
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
outputsBox.setPadding(new Insets(0, 20, 0, 10)); outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.BASELINE_LEFT); outputsBox.setAlignment(Pos.BASELINE_LEFT);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
@ -673,20 +722,26 @@ public class TransactionDiagram extends GridPane {
List<OutputNode> outputNodes = new ArrayList<>(); List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) { for(Payment payment : displayedPayments) {
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment); Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment; boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph); Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label"); recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label"); recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Wallet toBip47Wallet = getBip47SendWallet(payment); Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to " + getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()) + (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : "")); + (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE); recipientTooltip.setShowDuration(Duration.INDEFINITE);
recipientTooltip.setWrapText(true);
Window activeWindow = AppServices.getActiveWindow();
if(activeWindow != null) {
recipientTooltip.setMaxWidth(activeWindow.getWidth());
}
recipientLabel.setTooltip(recipientTooltip); recipientLabel.setTooltip(recipientTooltip);
HBox paymentBox = new HBox(); HBox paymentBox = new HBox();
paymentBox.setAlignment(Pos.CENTER_LEFT); paymentBox.setAlignment(Pos.CENTER_LEFT);
@ -702,7 +757,13 @@ public class TransactionDiagram extends GridPane {
paymentBox.getChildren().addAll(region, amountLabel); paymentBox.getChildren().addAll(region, amountLabel);
} }
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount())); if(payment instanceof SilentPayment silentPayment) {
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
} else {
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
}
} }
Set<Integer> seenIndexes = new HashSet<>(); Set<Integer> seenIndexes = new HashSet<>();
@ -766,7 +827,7 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(outputNode.outputLabel); outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer()); outputsBox.getChildren().add(createSpacer());
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount); ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode, outputNode.silentPaymentAddress);
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) { if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
outputLabelControl.setContextMenu(contextMenu); outputLabelControl.setContextMenu(contextMenu);
} }
@ -775,7 +836,7 @@ public class TransactionDiagram extends GridPane {
boolean highFee = (walletTx.getFeePercentage() > 0.1); boolean highFee = (walletTx.getFeePercentage() > 0.1);
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph()); Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label"); feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0); String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)"); Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
feeTooltip.getStyleClass().add("fee-tooltip"); feeTooltip.getStyleClass().add("fee-tooltip");
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
@ -829,6 +890,33 @@ public class TransactionDiagram extends GridPane {
return txPane; return txPane;
} }
private Pane getTotalsPane(List<Payment> userPayments) {
VBox totalsBox = new VBox();
totalsBox.setPadding(new Insets(0, 0, 15, 0));
totalsBox.setAlignment(Pos.CENTER);
long amount = userPayments.stream().mapToLong(Payment::getAmount).sum();
HBox coinLabelBox = new HBox();
coinLabelBox.setAlignment(Pos.CENTER);
CoinLabel totalCoinLabel = new CoinLabel();
totalCoinLabel.setValue(amount);
coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(userPayments.size())), new Label(" payments"));
totalsBox.getChildren().addAll(createSpacer(), coinLabelBox);
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
HBox fiatLabelBox = new HBox();
fiatLabelBox.setAlignment(Pos.CENTER);
FiatLabel fiatLabel = new FiatLabel();
fiatLabel.set(currencyRate, amount);
fiatLabelBox.getChildren().add(fiatLabel);
totalsBox.getChildren().add(fiatLabelBox);
}
return totalsBox;
}
private void saveAsImage() { private void saveAsImage() {
Stage window = new Stage(); Stage window = new Stage();
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
@ -914,8 +1002,11 @@ public class TransactionDiagram extends GridPane {
} }
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) { private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList()); List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow(); .map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
TransactionOutput output = addressOutputs.stream()
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
.findFirst().orElseThrow();
return addressOutputs.indexOf(output); return addressOutputs.indexOf(output);
} }
@ -1065,7 +1156,7 @@ public class TransactionDiagram extends GridPane {
} }
public String toString() { public String toString() {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n")); return additionalPayments.stream().map(Payment::toString).collect(Collectors.joining("\n"));
} }
} }
@ -1073,16 +1164,28 @@ public class TransactionDiagram extends GridPane {
public Pane outputLabel; public Pane outputLabel;
public Address address; public Address address;
public long amount; public long amount;
public PaymentCode paymentCode;
public SilentPaymentAddress silentPaymentAddress;
public OutputNode(Pane outputLabel, Address address, long amount) { public OutputNode(Pane outputLabel, Address address, long amount) {
this(outputLabel, address, amount, null, null);
}
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
this.outputLabel = outputLabel; this.outputLabel = outputLabel;
this.address = address; this.address = address;
this.amount = amount; this.amount = amount;
this.paymentCode = paymentCode;
this.silentPaymentAddress = silentPaymentAddress;
} }
} }
private class LabelContextMenu extends ContextMenu { private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Address address, long value) { public LabelContextMenu(Address address, long value) {
this(address, value, null, null);
}
public LabelContextMenu(Address address, long value, PaymentCode paymentCode, SilentPaymentAddress silentPaymentAddress) {
if(address != null) { if(address != null) {
MenuItem copyAddress = new MenuItem("Copy Address"); MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(event -> { copyAddress.setOnAction(event -> {
@ -1119,6 +1222,28 @@ public class TransactionDiagram extends GridPane {
Clipboard.getSystemClipboard().setContent(content); Clipboard.getSystemClipboard().setContent(content);
}); });
getItems().addAll(copySatsValue, copyBtcValue); getItems().addAll(copySatsValue, copyBtcValue);
if(paymentCode != null) {
MenuItem copyPaymentCode = new MenuItem("Copy Payment Code");
copyPaymentCode.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(paymentCode.toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyPaymentCode);
}
if(silentPaymentAddress != null) {
MenuItem copySilentPaymentAddress = new MenuItem("Copy Silent Payment Address");
copySilentPaymentAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(silentPaymentAddress.toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copySilentPaymentAddress);
}
} }
} }
} }

View file

@ -90,20 +90,20 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(mixOutputLabel); outputLabels.add(mixOutputLabel);
} }
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null } else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) { && walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && !walletTx.getWalletNodePayments().isEmpty()) {
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments()); OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
if(remixOutputLabel != null) { if(remixOutputLabel != null) {
outputLabels.add(remixOutputLabel); outputLabels.add(remixOutputLabel);
} }
} else { } else {
List<Payment> payments = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && !walletTx.isConsolidationSend(payment)).collect(Collectors.toList()); List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()); List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) { if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1))); paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
} }
outputLabels.addAll(paymentLabels); outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT && walletTx.isConsolidationSend(payment)).collect(Collectors.toList()); List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList())); outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList()); List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
@ -203,10 +203,10 @@ public class TransactionDiagramLabel extends HBox {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) { private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction(); WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment); Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null; WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment); Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString(); String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment;
return getOutputLabel(glyph, text); return getOutputLabel(glyph, text);
} }
@ -227,7 +227,8 @@ public class TransactionDiagramLabel extends HBox {
} }
Glyph glyph = GlyphUtils.getFeeGlyph(); Glyph glyph = GlyphUtils.getFeeGlyph();
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)"; String percentage = walletTx.getFeePercentage() < 0.0001d ? "<0.01" : String.format("%.2f", walletTx.getFeePercentage() * 100.0);
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)";
return getOutputLabel(glyph, text); return getOutputLabel(glyph, text);
} }
@ -239,7 +240,7 @@ public class TransactionDiagramLabel extends HBox {
icon.setGraphic(glyph); icon.setGraphic(glyph);
CopyableLabel label = new CopyableLabel(); CopyableLabel label = new CopyableLabel();
label.setFont(Font.font("Roboto Mono Italic", 13)); label.setFont(Font.font("Fragment Mono Italic", 13));
label.setText(text); label.setText(text);
HBox output = new HBox(5); HBox output = new HBox(5);

View file

@ -28,7 +28,8 @@ public class UsbStatusButton extends MenuButton {
for(Device device : devices) { for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString()); MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 || if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) { device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
device.getModel() == WalletModel.ONEKEY_CLASSIC_1S || device.getModel() == WalletModel.ONEKEY_PRO)) {
deviceItem = new Menu(device.getModel().toDisplayString()); deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On"))); MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
toggleItem.setOnAction(event -> { toggleItem.setOnAction(event -> {

View file

@ -18,8 +18,8 @@ import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> { public class WalletExportDialog extends Dialog<Wallet> {
private Wallet wallet; private Wallet wallet;
public WalletExportDialog(WalletForm walletForm) { public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
this.wallet = walletForm.getWallet(); this.wallet = selectedWalletForm.getWallet();
EventManager.get().register(this); EventManager.get().register(this);
setOnCloseRequest(event -> { setOnCloseRequest(event -> {
@ -45,10 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters; List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) { if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm)); exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI) { } else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm)); new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else { } else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType()); throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
} }

View file

@ -74,33 +74,15 @@ public class WalletIcon extends StackPane {
SVGImage svgImage; SVGImage svgImage;
if(Config.get().getTheme() == Theme.DARK) { if(Config.get().getTheme() == Theme.DARK) {
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg"); svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon-invert.svg");
} else { } else {
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg"); svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon.svg");
} }
if(svgImage != null) { if(svgImage != null) {
getChildren().add(svgImage); getChildren().add(svgImage);
return; return;
} }
Image image = null;
if(Config.get().getTheme() == Theme.DARK) {
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
}
if(image == null) {
image = loadImage("image/" + walletModel.getType() + "-icon.png");
}
if(image == null) {
image = loadImage("image/" + walletModel.getType() + ".png");
}
if(image != null && !image.isError()) {
ImageView imageView = new ImageView(image);
getChildren().add(imageView);
}
} }
} }
@ -127,16 +109,6 @@ public class WalletIcon extends StackPane {
return null; return null;
} }
private Image loadImage(String imageName) {
try {
return new Image(imageName, 15, 15, true, true);
} catch(Exception e) {
//ignore
}
return null;
}
private void addWalletIcon(String walletId) { private void addWalletIcon(String walletId) {
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false); Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
getChildren().clear(); getChildren().clear();

View file

@ -0,0 +1,78 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.io.Config;
import javafx.beans.NamedArg;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
public class WalletModelImage extends StackPane {
private static final Logger log = LoggerFactory.getLogger(WalletModelImage.class);
public static final int WIDTH = 50;
public static final int HEIGHT = 50;
private final ObjectProperty<WalletModel> walletModelProperty = new SimpleObjectProperty<>();
public WalletModelImage() {
setPrefSize(WIDTH, HEIGHT);
walletModelProperty.addListener((observable, oldValue, walletModel) -> {
refresh(walletModel);
});
}
public WalletModelImage(@NamedArg("walletModel") WalletModel walletModel) {
this();
walletModelProperty.set(walletModel);
}
public WalletModel getWalletModel() {
return walletModelProperty.get();
}
public ObjectProperty<WalletModel> walletModelProperty() {
return walletModelProperty;
}
public void refresh() {
WalletModel walletModel = getWalletModel();
refresh(walletModel);
}
protected void refresh(WalletModel walletModel) {
SVGImage svgImage;
if(Config.get().getTheme() == Theme.DARK) {
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-invert.svg");
} else {
svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + ".svg");
}
if(svgImage != null) {
getChildren().clear();
getChildren().add(svgImage);
}
}
private SVGImage loadSVGImage(String imageName) {
try {
URL url = AppServices.class.getResource(imageName);
if(url != null) {
return SVGLoader.load(url);
}
} catch(Exception e) {
log.error("Could not find image " + imageName);
}
return null;
}
}

View file

@ -17,6 +17,7 @@ import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.util.ArrayList; import java.util.ArrayList;
@ -43,14 +44,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
AppServices.setStageIcon(dialogPane.getScene().getWindow()); AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName())); dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if(!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
HBox hBox = new HBox(40); HBox hBox = new HBox(40);
@ -110,6 +104,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
vBox.getChildren().add(table); vBox.getChildren().add(table);
hBox.getChildren().add(vBox); hBox.getChildren().add(vBox);
HBox.setHgrow(vBox, Priority.ALWAYS);
Wallet balanceWallet; Wallet balanceWallet;
if(allOpenWallets) { if(allOpenWallets) {

View file

@ -0,0 +1,82 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OsType;
public enum WebcamPixelFormat {
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
PIX_FMT_RGB24("RGB3", true),
PIX_FMT_YUYV("YUYV", true),
PIX_FMT_NV12("NV12", true),
PIX_FMT_YU12("YU12", true),
PIX_FMT_MJPG("MJPG", true);
private final String name;
private final boolean supported;
WebcamPixelFormat(String name, boolean supported) {
this.name = name;
this.supported = supported;
}
public String getName() {
return name;
}
public boolean isSupported() {
return supported;
}
public int getFourCC() {
char a = name.charAt(0);
char b = name.charAt(1);
char c = name.charAt(2);
char d = name.charAt(3);
return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
}
public String toString() {
return name;
}
public static WebcamPixelFormat fromFourCC(int fourCC) {
String strFourCC = fourCCToString(fourCC);
for(WebcamPixelFormat pixelFormat : WebcamPixelFormat.values()) {
if(pixelFormat.getName().equalsIgnoreCase(strFourCC)) {
return pixelFormat;
}
}
return null;
}
public static String fourCCToString(int fourCC) {
int fccVal = fourCC;
int tmp = fccVal;
if(OsType.getCurrent() == OsType.MACOS) {
tmp = ((tmp >> 16) & 0x0000FFFF) | ((tmp << 16) & 0xFFFF0000);
tmp = ((tmp & 0x00FF00FF) << 8) | ((tmp & 0xFF00FF00) >>> 8);
}
fccVal = tmp;
StringBuilder v = new StringBuilder(4);
for(int i = 0; i < 4; i++) {
char c = (char) (fccVal & 0xFF);
v.append(c);
fccVal >>>= 8;
}
return v.toString();
}
public static int getPriority(WebcamPixelFormat pixelFormat) {
if(pixelFormat == null) {
return values().length;
} else if(pixelFormat.isSupported()) {
return pixelFormat.ordinal();
} else {
return values().length + 1;
}
}
}

View file

@ -0,0 +1,71 @@
package com.sparrowwallet.sparrow.control;
import org.openpnp.capture.CaptureFormat;
import java.util.Arrays;
public enum WebcamResolution implements Comparable<WebcamResolution> {
VGA("480p", 640, 480),
HD("720p", 1280, 720),
FHD("1080p", 1920, 1080),
UHD4K("4K", 3840, 2160);
private final String name;
private final int width;
private final int height;
WebcamResolution(String name, int width, int height) {
this.name = name;
this.width = width;
this.height = height;
}
public int getPixelsCount() {
return this.width * this.height;
}
public boolean isStandardAspect() {
return Arrays.equals(getAspectRatio(), new int[]{4, 3});
}
public boolean isWidescreenAspect() {
return Arrays.equals(getAspectRatio(), new int[]{16, 9});
}
public int[] getAspectRatio() {
int factor = this.getCommonFactor(this.width, this.height);
int wr = this.width / factor;
int hr = this.height / factor;
return new int[] {wr, hr};
}
private int getCommonFactor(int width, int height) {
return height == 0 ? width : this.getCommonFactor(height, width % height);
}
public String getName() {
return name;
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public String toString() {
return name;
}
public static WebcamResolution from(CaptureFormat captureFormat) {
for(WebcamResolution resolution : values()) {
if(captureFormat.getFormatInfo().width == resolution.width && captureFormat.getFormatInfo().height == resolution.height) {
return resolution;
}
}
return null;
}
}

View file

@ -1,372 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.github.sarxos.webcam.ds.buildin.natives.Device;
import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
import org.bridj.Pointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.nio.ByteBuffer;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings("deprecation")
public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess, Runnable, WebcamDevice.FPSSource {
private static final Logger LOG = LoggerFactory.getLogger(WebcamScanDevice.class);
private static final int DEVICE_BUFFER_SIZE = 5;
private static final Dimension[] DIMENSIONS;
private static final int[] BAND_OFFSETS;
private static final int[] BITS;
private static final int[] OFFSET;
private static final int DATA_TYPE = 0;
private static final ColorSpace COLOR_SPACE;
public static final int SCAN_LOOP_WAIT_MILLIS = 100;
private int timeout = 5000;
private OpenIMAJGrabber grabber = null;
private Device device = null;
private Dimension size = null;
private ComponentSampleModel smodel = null;
private ColorModel cmodel = null;
private boolean failOnSizeMismatch = false;
private final AtomicBoolean disposed = new AtomicBoolean(false);
private final AtomicBoolean open = new AtomicBoolean(false);
private final AtomicBoolean fresh = new AtomicBoolean(false);
private Thread refresher = null;
private String name = null;
private String id = null;
private String fullname = null;
private long t1 = -1L;
private long t2 = -1L;
private volatile double fps = 0.0D;
protected WebcamScanDevice(Device device) {
this.device = device;
this.name = device.getNameStr();
this.id = device.getIdentifierStr();
this.fullname = String.format("%s %s", this.name, this.id);
}
public String getName() {
return this.fullname;
}
public String getDeviceName() {
return this.name;
}
public String getDeviceId() {
return this.id;
}
public Device getDeviceRef() {
return this.device;
}
public Dimension[] getResolutions() {
return DIMENSIONS;
}
public Dimension getResolution() {
if (this.size == null) {
this.size = this.getResolutions()[0];
}
return this.size;
}
public void setResolution(Dimension size) {
if (size == null) {
throw new IllegalArgumentException("Size cannot be null");
} else if (this.open.get()) {
throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
} else {
this.size = size;
}
}
public ByteBuffer getImageBytes() {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
return null;
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
return null;
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
return null;
} else {
int length = this.size.width * this.size.height * 3;
LOG.trace("Webcam device get buffer, read {} bytes", length);
return image.getByteBuffer((long)length);
}
}
}
public void getImageBytes(ByteBuffer target) {
if (this.disposed.get()) {
LOG.debug("Webcam is disposed, image will be null");
} else if (!this.open.get()) {
LOG.debug("Webcam is closed, image will be null");
} else {
int minSize = this.size.width * this.size.height * 3;
int curSize = target.remaining();
if (minSize > curSize) {
throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
} else {
if (this.fresh.compareAndSet(false, true)) {
this.updateFrameBuffer();
}
LOG.trace("Webcam grabber get image pointer");
Pointer<Byte> image = this.grabber.getImage();
this.fresh.set(false);
if (image == null) {
LOG.warn("Null array pointer found instead of image");
} else {
LOG.trace("Webcam device read buffer {} bytes", minSize);
image = image.validBytes((long)minSize);
image.getBytes(target);
}
}
}
}
public BufferedImage getImage() {
ByteBuffer buffer = this.getImageBytes();
if (buffer == null) {
LOG.error("Images bytes buffer is null!");
return null;
} else {
byte[] bytes = new byte[this.size.width * this.size.height * 3];
byte[][] data = new byte[][]{bytes};
buffer.get(bytes);
DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
WritableRaster raster = Raster.createWritableRaster(this.smodel, dbuf, (Point)null);
BufferedImage bi = new BufferedImage(this.cmodel, raster, false, (Hashtable)null);
bi.flush();
return bi;
}
}
public void open() {
if (!this.disposed.get()) {
LOG.debug("Opening webcam device {}", this.getName());
if (this.size == null) {
this.size = this.getResolutions()[0];
}
if (this.size == null) {
throw new RuntimeException("The resolution size cannot be null");
} else {
LOG.debug("Webcam device {} starting session, size {}", this.device.getIdentifierStr(), this.size);
this.grabber = new OpenIMAJGrabber();
DeviceList list = (DeviceList)this.grabber.getVideoDevices().get();
Iterator var2 = list.asArrayList().iterator();
while(var2.hasNext()) {
Device d = (Device)var2.next();
d.getNameStr();
d.getIdentifierStr();
}
boolean started = this.grabber.startSession(this.size.width, this.size.height, 50, Pointer.pointerTo(this.device));
if (!started) {
throw new WebcamException("Cannot start native grabber!");
} else {
this.grabber.setTimeout(this.timeout);
LOG.debug("Webcam device session started");
Dimension size2 = new Dimension(this.grabber.getWidth(), this.grabber.getHeight());
int w1 = this.size.width;
int w2 = size2.width;
int h1 = this.size.height;
int h2 = size2.height;
if (w1 != w2 || h1 != h2) {
if (this.failOnSizeMismatch) {
throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
}
Object[] args = new Object[]{w1, h1, w2, h2, w2, h2};
LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
this.size = new Dimension(w2, h2);
}
this.smodel = new ComponentSampleModel(0, this.size.width, this.size.height, 3, this.size.width * 3, BAND_OFFSETS);
this.cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, 1, 0);
LOG.debug("Clear memory buffer");
this.clearMemoryBuffer();
LOG.debug("Webcam device {} is now open", this);
this.open.set(true);
this.refresher = this.startFramesRefresher();
}
}
}
}
private void clearMemoryBuffer() {
for(int i = 0; i < 5; ++i) {
this.grabber.nextFrame();
}
}
private Thread startFramesRefresher() {
Thread refresher = new Thread(this, String.format("frames-refresher-[%s]", this.id));
refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
refresher.setDaemon(true);
refresher.start();
return refresher;
}
public void close() {
if (this.open.compareAndSet(true, false)) {
LOG.debug("Closing webcam device");
this.grabber.stopSession();
}
}
public void dispose() {
if (this.disposed.compareAndSet(false, true)) {
LOG.debug("Disposing webcam device {}", this.getName());
this.close();
}
}
public void setFailOnSizeMismatch(boolean fail) {
this.failOnSizeMismatch = fail;
}
public boolean isOpen() {
return this.open.get();
}
public int getTimeout() {
return this.timeout;
}
public void setTimeout(int timeout) {
if (this.isOpen()) {
throw new WebcamException("Timeout must be set before webcam is open");
} else {
this.timeout = timeout;
}
}
private void updateFrameBuffer() {
LOG.trace("Next frame");
if (this.t1 == -1L || this.t2 == -1L) {
this.t1 = System.currentTimeMillis();
this.t2 = System.currentTimeMillis();
}
int result = (new WebcamScanDevice.NextFrameTask(this)).nextFrame();
this.t1 = this.t2;
this.t2 = System.currentTimeMillis();
this.fps = (4.0D * this.fps + (double)(1000L / (this.t2 - this.t1 + 1L))) / 5.0D;
if (result == -1) {
LOG.error("Timeout when requesting image!");
} else if (result < -1) {
LOG.error("Error requesting new frame!");
}
}
public void run() {
do {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
if (Thread.interrupted()) {
LOG.debug("Refresher has been interrupted");
return;
}
if (!this.open.get()) {
LOG.debug("Cancelling refresher");
return;
}
this.updateFrameBuffer();
} while(this.open.get());
}
public double getFPS() {
return this.fps;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
WebcamScanDevice that = (WebcamScanDevice) o;
return Objects.equals(fullname, that.fullname);
}
@Override
public int hashCode() {
return Objects.hash(fullname);
}
static {
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
BAND_OFFSETS = new int[]{0, 1, 2};
BITS = new int[]{8, 8, 8};
OFFSET = new int[]{0};
COLOR_SPACE = ColorSpace.getInstance(1000);
}
private class NextFrameTask extends WebcamTask {
private final AtomicInteger result = new AtomicInteger(0);
public NextFrameTask(WebcamDevice device) {
super(device);
}
public int nextFrame() {
try {
this.process();
} catch (InterruptedException var2) {
WebcamScanDevice.LOG.debug("Image buffer request interrupted", var2);
}
return this.result.get();
}
protected void handle() {
WebcamScanDevice device = (WebcamScanDevice)this.getDevice();
if (device.isOpen()) {
try {
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
} catch(InterruptedException e) {
//ignore
}
this.result.set(WebcamScanDevice.this.grabber.nextFrame());
WebcamScanDevice.this.fresh.set(true);
}
}
}
}

View file

@ -1,45 +0,0 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
public class WebcamScanDriver extends WebcamDefaultDriver {
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
private static boolean rescan;
@Override
public List<WebcamDevice> getDevices() {
if(rescan || webcamDevices.isEmpty()) {
List<WebcamDevice> devices = super.getDevices();
List<WebcamDevice> scanDevices = new ArrayList<>();
for(WebcamDevice device : devices) {
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
scanDevices.add(scanDevice);
}
}
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
newDevices.removeAll(webcamDevices);
webcamDevices.addAll(newDevices);
webcamDevices.removeIf(device -> !scanDevices.contains(device));
}
return webcamDevices;
}
public static ObservableList<WebcamDevice> getFoundDevices() {
return webcamDevices;
}
public static void rescan() {
rescan = true;
}
}

View file

@ -1,12 +1,13 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.*;
import com.google.zxing.*; import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer; import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader; import com.google.zxing.qrcode.QRCodeReader;
import com.sparrowwallet.bokmakierie.Bokmakierie; import com.sparrowwallet.bokmakierie.Bokmakierie;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ZBar;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -15,7 +16,8 @@ import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils; import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import net.sourceforge.zbar.ZBar; import org.openpnp.capture.*;
import org.openpnp.capture.library.OpenpnpCaptureLibrary;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -23,38 +25,84 @@ import java.awt.*;
import java.awt.geom.RoundRectangle2D; import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster; import java.awt.image.WritableRaster;
import java.util.Arrays; import java.util.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WebcamService extends ScheduledService<Image> { public class WebcamService extends ScheduledService<Image> {
private static final Logger log = LoggerFactory.getLogger(WebcamService.class); private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
private final Semaphore taskSemaphore = new Semaphore(1);
private final AtomicBoolean cancelRequested = new AtomicBoolean(false);
private final AtomicBoolean captureClosed = new AtomicBoolean(false);
private List<CaptureDevice> devices;
private List<CaptureDevice> availableDevices;
private Set<WebcamResolution> resolutions;
private WebcamResolution resolution; private WebcamResolution resolution;
private WebcamDevice device; private CaptureDevice device;
private final WebcamListener listener;
private final WebcamUpdater.DelayCalculator delayCalculator;
private final BooleanProperty opening = new SimpleBooleanProperty(false); private final BooleanProperty opening = new SimpleBooleanProperty(false);
private final BooleanProperty opened = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null); private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
private static final int QR_SAMPLE_PERIOD_MILLIS = 200; private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
private Webcam cam; private final OpenPnpCapture capture;
private CaptureStream stream;
private PropertyLimits zoomLimits;
private long lastQrSampleTime; private long lastQrSampleTime;
private final Reader qrReader; private final Reader qrReader;
private final Bokmakierie bokmakierie; private final Bokmakierie bokmakierie;
static { static {
Webcam.setDriver(new WebcamScanDriver()); if(log.isTraceEnabled()) {
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(8);
} else if(log.isDebugEnabled()) {
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(7);
} else if(log.isInfoEnabled()) {
OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(6);
}
OpenpnpCaptureLibrary.INSTANCE.Cap_installCustomLogFunction((level, ptr) -> {
switch(level) {
case 0:
case 1:
case 2:
case 3:
String err = ptr.getString(0).trim();
if(err.equals("tjDecompressHeader2 failed: No error") || err.matches("getPropertyLimits.*failed on.*")) { //Safe to ignore
log.debug(err);
} else {
log.error(err);
}
break;
case 4:
case 5:
case 6:
log.info(ptr.getString(0).trim());
break;
case 7:
log.debug(ptr.getString(0).trim());
break;
case 8:
default:
log.trace(ptr.getString(0).trim());
break;
}
});
} }
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) { public WebcamService(WebcamResolution requestedResolution, CaptureDevice requestedDevice) {
this.resolution = resolution; this.capture = new OpenPnpCapture();
this.device = device; this.resolution = requestedResolution;
this.listener = listener; this.device = requestedDevice;
this.delayCalculator = delayCalculator;
this.lastQrSampleTime = System.currentTimeMillis(); this.lastQrSampleTime = System.currentTimeMillis();
this.qrReader = new QRCodeReader(); this.qrReader = new QRCodeReader();
this.bokmakierie = new Bokmakierie(); this.bokmakierie = new Bokmakierie();
@ -62,50 +110,115 @@ public class WebcamService extends ScheduledService<Image> {
@Override @Override
public Task<Image> createTask() { public Task<Image> createTask() {
return new Task<Image>() { return new Task<>() {
@Override @Override
protected Image call() throws Exception { protected Image call() throws Exception {
try { if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
if(cam == null) { return null;
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES); }
if(webcams.isEmpty()) {
throw new UnsupportedOperationException("No camera available.");
}
cam = webcams.get(0); if(!taskSemaphore.tryAcquire()) {
log.warn("Skipped execution of webcam capture task, another task is running");
return null;
}
try {
if(devices == null) {
devices = capture.getDevices();
availableDevices = new ArrayList<>(devices);
if(devices.isEmpty()) {
throw new UnsupportedOperationException("No cameras available");
}
}
while(stream == null && !availableDevices.isEmpty()) {
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
if(device != null) { if(device != null) {
for(Webcam webcam : webcams) { for(CaptureDevice webcam : availableDevices) {
if(webcam.getDevice().getName().equals(device.getName())) { if(webcam.equals(device)) {
cam = webcam; selectedDevice = webcam;
break;
} }
} }
} else if(Config.get().getWebcamDevice() != null) { } else if(Config.get().getWebcamDevice() != null) {
for(Webcam webcam : webcams) { for(CaptureDevice webcam : availableDevices) {
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) { if(webcam.getUniqueId().equals(Config.get().getWebcamDeviceId())) {
cam = webcam; selectedDevice = webcam;
break;
}
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
selectedDevice = webcam;
break;
} }
} }
} }
device = cam.getDevice(); device = selectedDevice;
cam.setCustomViewSizes(resolution.getSize()); if(device.getFormats().isEmpty()) {
cam.setViewSize(resolution.getSize()); throw new UnsupportedOperationException("No resolutions supported by camera " + device.getName());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) { }
cam.addWebcamListener(listener);
List<CaptureFormat> deviceFormats = new ArrayList<>(device.getFormats());
//On *nix prioritise supported camera pixel formats, preferring RGB3, then YUYV, then MJPG
//On macOS and Windows, camera pixel format is largely abstracted away
if(OsType.getCurrent() == OsType.UNIX) {
deviceFormats.sort((f1, f2) -> {
WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.getFormatInfo().fourcc);
WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.getFormatInfo().fourcc);
return Integer.compare(WebcamPixelFormat.getPriority(pf1), WebcamPixelFormat.getPriority(pf2));
});
}
Map<WebcamResolution, CaptureFormat> supportedResolutions = deviceFormats.stream()
.filter(f -> WebcamResolution.from(f) != null)
.collect(Collectors.toMap(WebcamResolution::from, Function.identity(), (u, v) -> u, TreeMap::new));
resolutions = supportedResolutions.keySet();
CaptureFormat format = supportedResolutions.get(resolution);
if(format == null) {
if(!supportedResolutions.isEmpty()) {
resolution = getNearestEnum(resolution, supportedResolutions.keySet().toArray(new WebcamResolution[0]));
format = supportedResolutions.get(resolution);
} else {
format = device.getFormats().getFirst();
log.warn("Could not get standard capture resolution, using " + format.getFormatInfo().width + "x" + format.getFormatInfo().height);
}
}
//On Linux, formats not defined in WebcamPixelFormat are unsupported
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
}
if(log.isDebugEnabled()) {
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
} }
opening.set(true); opening.set(true);
cam.open(true, delayCalculator); stream = device.openStream(format);
opening.set(false); opening.set(false);
try {
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
} catch(Throwable e) {
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
}
if(stream == null) {
availableDevices.remove(device);
}
} }
BufferedImage originalImage = cam.getImage(); if(stream == null) {
if(originalImage == null) { throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
return null;
} }
opened.set(true);
BufferedImage originalImage = stream.capture();
CroppedDimension cropped = getCroppedDimension(originalImage); CroppedDimension cropped = getCroppedDimension(originalImage);
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length); BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
BufferedImage framedImage = getFramedImage(originalImage, cropped); BufferedImage framedImage = getFramedImage(originalImage, cropped);
@ -121,6 +234,7 @@ public class WebcamService extends ScheduledService<Image> {
return image; return image;
} finally { } finally {
opening.set(false); opening.set(false);
taskSemaphore.release();
} }
} }
}; };
@ -128,17 +242,66 @@ public class WebcamService extends ScheduledService<Image> {
@Override @Override
public void reset() { public void reset() {
cam = null; stream = null;
zoomLimits = null;
cancelRequested.set(false);
super.reset(); super.reset();
} }
@Override @Override
public boolean cancel() { public boolean cancel() {
if(cam != null && !cam.close()) { cancelRequested.set(true);
cam.close(); boolean cancelled = super.cancel();
try {
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
taskSemaphore.release();
} else {
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
}
} catch(InterruptedException e) {
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
} }
return super.cancel(); if(stream != null) {
stream.close();
opened.set(false);
}
return cancelled;
}
public synchronized void close() {
if(!captureClosed.get()) {
captureClosed.set(true);
capture.close();
}
}
public PropertyLimits getZoomLimits() {
return zoomLimits;
}
public int getZoom() {
if(stream != null && zoomLimits != null) {
try {
return stream.getProperty(CaptureProperty.Zoom);
} catch(Exception e) {
log.error("Error getting zoom property on " + device, e);
}
}
return -1;
}
public void setZoom(int value) {
if(stream != null && zoomLimits != null) {
try {
stream.setProperty(CaptureProperty.Zoom, value);
} catch(Exception e) {
log.error("Error setting zoom property on " + device, e);
}
}
} }
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) { private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
@ -156,9 +319,6 @@ public class WebcamService extends ScheduledService<Image> {
} }
private Result readQR(BufferedImage bufferedImage) { private Result readQR(BufferedImage bufferedImage) {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try { try {
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage); com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
if(result != null) { if(result != null) {
@ -176,6 +336,8 @@ public class WebcamService extends ScheduledService<Image> {
} }
try { try {
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE)); return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
} catch(ReaderException e) { } catch(ReaderException e) {
// fall thru, it means there is no QR code in image // fall thru, it means there is no QR code in image
@ -189,7 +351,7 @@ public class WebcamService extends ScheduledService<Image> {
g2d.drawImage(image, 0, 0, null); g2d.drawImage(image, 0, 0, null);
float[] dash1 = {10.0f}; float[] dash1 = {10.0f};
g2d.setColor(Color.BLACK); g2d.setColor(Color.BLACK);
g2d.setStroke(new BasicStroke(resolution == WebcamResolution.HD ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f)); g2d.setStroke(new BasicStroke(resolution.isWidescreenAspect() ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10)); g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10));
g2d.dispose(); g2d.dispose();
return clone; return clone;
@ -226,6 +388,18 @@ public class WebcamService extends ScheduledService<Image> {
} }
} }
public List<CaptureDevice> getDevices() {
return devices;
}
public List<CaptureDevice> getAvailableDevices() {
return availableDevices;
}
public Set<WebcamResolution> getResolutions() {
return resolutions;
}
public Result getResult() { public Result getResult() {
return resultProperty.get(); return resultProperty.get();
} }
@ -235,33 +409,69 @@ public class WebcamService extends ScheduledService<Image> {
} }
public int getCamWidth() { public int getCamWidth() {
return resolution.getSize().width; return resolution.getWidth();
} }
public int getCamHeight() { public int getCamHeight() {
return resolution.getSize().height; return resolution.getHeight();
}
public WebcamResolution getResolution() {
return resolution;
} }
public void setResolution(WebcamResolution resolution) { public void setResolution(WebcamResolution resolution) {
this.resolution = resolution; this.resolution = resolution;
} }
public WebcamDevice getDevice() { public CaptureDevice getDevice() {
return device; return device;
} }
public void setDevice(WebcamDevice device) { public void setDevice(CaptureDevice device) {
this.device = device; this.device = device;
} }
public boolean isOpening() {
return opening.get();
}
public BooleanProperty openingProperty() { public BooleanProperty openingProperty() {
return opening; return opening;
} }
public BooleanProperty openedProperty() {
return opened;
}
public boolean getCancelRequested() {
return cancelRequested.get();
}
public static <T extends Enum<T>> T getNearestEnum(T target) {
return getNearestEnum(target, target.getDeclaringClass().getEnumConstants());
}
public static <T extends Enum<T>> T getNearestEnum(T target, T[] values) {
if(values == null || values.length == 0) {
return null;
}
int targetOrdinal = target.ordinal();
if(values.length == 1) {
return values[0];
}
for(int i = 0; i < values.length; i++) {
if(targetOrdinal < values[i].ordinal()) {
if(i == 0) {
return values[0];
}
int diffToPrev = Math.abs(targetOrdinal - values[i - 1].ordinal());
int diffToNext = Math.abs(targetOrdinal - values[i].ordinal());
return diffToPrev <= diffToNext ? values[i - 1] : values[i];
}
}
return values[values.length - 1];
}
private static class CroppedDimension { private static class CroppedDimension {
public int x; public int x;
public int y; public int y;

View file

@ -46,9 +46,22 @@ public class WebcamView {
imageView.setOnContextMenuRequested(event -> { imageView.setOnContextMenuRequested(event -> {
contextMenu.show(imageView, event.getScreenX(), event.getScreenY()); contextMenu.show(imageView, event.getScreenX(), event.getScreenY());
}); });
imageView.setOnScroll(scrollEvent -> {
if(service.isRunning() && scrollEvent.getDeltaY() != 0 && service.getZoomLimits() != null) {
int currentZoom = service.getZoom();
if(currentZoom >= 0) {
int newZoom = scrollEvent.getDeltaY() > 0 ? Math.round(currentZoom * 1.1f) : Math.round(currentZoom * 0.9f);
newZoom = Math.max(newZoom, service.getZoomLimits().getMin());
newZoom = Math.min(newZoom, service.getZoomLimits().getMax());
if(newZoom != currentZoom) {
service.setZoom(newZoom);
}
}
}
});
service.valueProperty().addListener((observable, oldValue, newValue) -> { service.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) { if(newValue != null && !service.getCancelRequested()) {
imageProperty.set(newValue); imageProperty.set(newValue);
} }
}); });
@ -57,27 +70,29 @@ public class WebcamView {
this.view = new Region() { this.view = new Region() {
{ {
service.stateProperty().addListener((obs, oldState, newState) -> { service.stateProperty().addListener((obs, oldState, newState) -> {
switch (newState) { switch(newState) {
case READY: case READY:
if(imageProperty.get() == null) { if(imageProperty.get() == null) {
statusPlaceholder.setText("Initializing"); statusPlaceholder.setText("Initializing");
getChildren().setAll(statusPlaceholder); getChildren().setAll(statusPlaceholder);
} }
break ; break;
case SCHEDULED: case SCHEDULED:
if(imageProperty.get() == null) { if(imageProperty.get() == null) {
statusPlaceholder.setText("Waiting"); statusPlaceholder.setText("Waiting");
getChildren().setAll(statusPlaceholder); getChildren().setAll(statusPlaceholder);
} }
break ; break;
case RUNNING: case RUNNING:
imageView.imageProperty().unbind(); if(imageProperty.get() == null) {
imageView.imageProperty().bind(imageProperty); imageView.imageProperty().unbind();
getChildren().setAll(imageView); imageView.imageProperty().bind(imageProperty);
break ; getChildren().setAll(imageView);
}
break;
case CANCELLED: case CANCELLED:
imageProperty.set(null);
imageView.imageProperty().unbind(); imageView.imageProperty().unbind();
imageView.setImage(null);
statusPlaceholder.setText("Stopped"); statusPlaceholder.setText("Stopped");
getChildren().setAll(statusPlaceholder); getChildren().setAll(statusPlaceholder);
break; break;
@ -93,7 +108,6 @@ public class WebcamView {
statusPlaceholder.setText(""); statusPlaceholder.setText("");
getChildren().clear(); getChildren().clear();
} }
requestLayout();
}); });
} }
@ -102,14 +116,14 @@ public class WebcamView {
super.layoutChildren(); super.layoutChildren();
double w = getWidth(); double w = getWidth();
double h = getHeight(); double h = getHeight();
if (service.isRunning()) { if(service.isRunning()) {
imageView.setFitWidth(w); imageView.setFitWidth(w);
imageView.setFitHeight(h); imageView.setFitHeight(h);
imageView.resizeRelocate(0, 0, w, h); imageView.resizeRelocate(0, 0, w, h);
} else { } else {
double labelHeight = statusPlaceholder.prefHeight(w); double labelHeight = statusPlaceholder.prefHeight(w);
double labelWidth = statusPlaceholder.prefWidth(labelHeight); double labelWidth = statusPlaceholder.prefWidth(labelHeight);
statusPlaceholder.resizeRelocate((w - labelWidth)/2, (h-labelHeight)/2, labelWidth, labelHeight); statusPlaceholder.resizeRelocate((w - labelWidth) / 2, (h - labelHeight) / 2, labelWidth, labelHeight);
} }
} }

View file

@ -36,7 +36,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
private ExtendedKey xprv; private ExtendedKey xprv;
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) { public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) {
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png"); super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), importer.getWalletModel());
this.wallet = wallet; this.wallet = wallet;
this.importer = importer; this.importer = importer;
this.defaultDerivation = defaultDerivation; this.defaultDerivation = defaultDerivation;
@ -46,7 +46,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
} }
public XprvKeystoreImportPane(Keystore keystore) { public XprvKeystoreImportPane(Keystore keystore) {
super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png"); super("Master Private Key", "BIP32 key", "", WalletModel.SEED);
this.wallet = null; this.wallet = null;
this.importer = null; this.importer = null;
this.defaultDerivation = keystore.getKeyDerivation(); this.defaultDerivation = keystore.getKeyDerivation();

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.BlockSummary;
import java.util.Map;
public class BlockSummaryEvent {
private final Map<Integer, BlockSummary> blockSummaryMap;
private final Double nextBlockMedianFeeRate;
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
this.blockSummaryMap = blockSummaryMap;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
}
public Map<Integer, BlockSummary> getBlockSummaryMap() {
return blockSummaryMap;
}
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader; import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.net.MempoolRateSize; import com.sparrowwallet.sparrow.net.MempoolRateSize;
import java.util.List; import java.util.List;
@ -13,6 +14,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
private final int blockHeight; private final int blockHeight;
private final BlockHeader blockHeader; private final BlockHeader blockHeader;
private final Double minimumRelayFeeRate; private final Double minimumRelayFeeRate;
private final Double previousMinimumRelayFeeRate;
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) { public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
super(targetBlockFeeRates, mempoolRateSizes); super(targetBlockFeeRates, mempoolRateSizes);
@ -21,6 +23,7 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
this.blockHeight = blockHeight; this.blockHeight = blockHeight;
this.blockHeader = blockHeader; this.blockHeader = blockHeader;
this.minimumRelayFeeRate = minimumRelayFeeRate; this.minimumRelayFeeRate = minimumRelayFeeRate;
this.previousMinimumRelayFeeRate = AppServices.getMinimumRelayFeeRate();
} }
public List<String> getServerVersion() { public List<String> getServerVersion() {
@ -42,4 +45,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
public Double getMinimumRelayFeeRate() { public Double getMinimumRelayFeeRate() {
return minimumRelayFeeRate; return minimumRelayFeeRate;
} }
public Double getPreviousMinimumRelayFeeRate() {
return previousMinimumRelayFeeRate;
}
} }

View file

@ -7,13 +7,23 @@ import java.util.Set;
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent { public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates; private final Map<Integer, Double> targetBlockFeeRates;
private final Double nextBlockMedianFeeRate;
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) { public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
this(targetBlockFeeRates, mempoolRateSizes, null);
}
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
super(mempoolRateSizes); super(mempoolRateSizes);
this.targetBlockFeeRates = targetBlockFeeRates; this.targetBlockFeeRates = targetBlockFeeRates;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
} }
public Map<Integer, Double> getTargetBlockFeeRates() { public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates; return targetBlockFeeRates;
} }
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
} }

View file

@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Payment;
import java.util.List;
public class RequestSendToManyEvent {
private final List<Payment> payments;
public RequestSendToManyEvent(List<Payment> payments) {
this.payments = payments;
}
public List<Payment> getPayments() {
return payments;
}
}

View file

@ -17,12 +17,13 @@ public class SpendUtxoEvent {
private final boolean requireAllUtxos; private final boolean requireAllUtxos;
private final BlockTransaction replacedTransaction; private final BlockTransaction replacedTransaction;
private final PaymentCode paymentCode; private final PaymentCode paymentCode;
private final boolean allowPaymentChanges;
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this(wallet, utxos, null, null, null, false, null); this(wallet, utxos, null, null, null, false, null, true);
} }
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction) { public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean requireAllUtxos, BlockTransaction replacedTransaction, boolean allowPaymentChanges) {
this.wallet = wallet; this.wallet = wallet;
this.utxos = utxos; this.utxos = utxos;
this.payments = payments; this.payments = payments;
@ -31,6 +32,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = requireAllUtxos; this.requireAllUtxos = requireAllUtxos;
this.replacedTransaction = replacedTransaction; this.replacedTransaction = replacedTransaction;
this.paymentCode = null; this.paymentCode = null;
this.allowPaymentChanges = allowPaymentChanges;
} }
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) { public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
@ -42,6 +44,7 @@ public class SpendUtxoEvent {
this.requireAllUtxos = false; this.requireAllUtxos = false;
this.replacedTransaction = null; this.replacedTransaction = null;
this.paymentCode = paymentCode; this.paymentCode = paymentCode;
this.allowPaymentChanges = false;
} }
public Wallet getWallet() { public Wallet getWallet() {
@ -75,4 +78,8 @@ public class SpendUtxoEvent {
public PaymentCode getPaymentCode() { public PaymentCode getPaymentCode() {
return paymentCode; return paymentCode;
} }
public boolean allowPaymentChanges() {
return allowPaymentChanges;
}
} }

View file

@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
public class TransactionOutputsChangedEvent extends TransactionChangedEvent {
public TransactionOutputsChangedEvent(Transaction transaction) {
super(transaction);
}
}

View file

@ -14,9 +14,16 @@ import java.util.List;
*/ */
public class WalletNodeHistoryChangedEvent { public class WalletNodeHistoryChangedEvent {
private final String scriptHash; private final String scriptHash;
private final String status;
public WalletNodeHistoryChangedEvent(String scriptHash) { public WalletNodeHistoryChangedEvent(String scriptHash) {
this.scriptHash = scriptHash; this.scriptHash = scriptHash;
this.status = null;
}
public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
this.scriptHash = scriptHash;
this.status = status;
} }
public WalletNode getWalletNode(Wallet wallet) { public WalletNode getWalletNode(Wallet wallet) {
@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
public String getScriptHash() { public String getScriptHash() {
return scriptHash; return scriptHash;
} }
public String getStatus() {
return status;
}
} }

View file

@ -1,13 +1,15 @@
package com.sparrowwallet.sparrow.event; package com.sparrowwallet.sparrow.event;
public class WebcamResolutionChangedEvent { import com.sparrowwallet.sparrow.control.WebcamResolution;
private final boolean hdResolution;
public WebcamResolutionChangedEvent(boolean hdResolution) { public class WebcamResolutionChangedEvent {
this.hdResolution = hdResolution; private final WebcamResolution resolution;
public WebcamResolutionChangedEvent(WebcamResolution resolution) {
this.resolution = resolution;
} }
public boolean isHdResolution() { public WebcamResolution getResolution() {
return hdResolution; return resolution;
} }
} }

View file

@ -16,6 +16,7 @@ public class FontAwesome5 extends GlyphFont {
*/ */
public static enum Glyph implements INamedCharacter { public static enum Glyph implements INamedCharacter {
ADJUST('\uf042'), ADJUST('\uf042'),
ANCHOR('\uf13d'),
ARROW_CIRCLE_DOWN('\uf0ab'), ARROW_CIRCLE_DOWN('\uf0ab'),
ANGLE_DOUBLE_RIGHT('\uf101'), ANGLE_DOUBLE_RIGHT('\uf101'),
ARROW_DOWN('\uf063'), ARROW_DOWN('\uf063'),

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.glyphfont; package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment; import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
import com.sparrowwallet.drongo.wallet.WalletTransaction; import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram; import com.sparrowwallet.sparrow.control.TransactionDiagram;
@ -13,7 +14,9 @@ public class GlyphUtils {
return getMixGlyph(); return getMixGlyph();
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) { } else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph(); return getFakeMixGlyph();
} else if(walletTx.isConsolidationSend(payment)) { } else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph();
} else if(payment instanceof WalletNodePayment) {
return getConsolidationGlyph(); return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) { } else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph(); return getPremixGlyph();
@ -211,10 +214,24 @@ public class GlyphUtils {
return busyGlyph; return busyGlyph;
} }
public static Glyph getUpArrowGlyph() {
Glyph upGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_UP);
upGlyph.getStyleClass().add("arrow-up");
upGlyph.setFontSize(12);
return upGlyph;
}
public static Glyph getDownArrowGlyph() { public static Glyph getDownArrowGlyph() {
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN); Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
downGlyph.getStyleClass().add("arrow-down"); downGlyph.getStyleClass().add("arrow-down");
downGlyph.setFontSize(12); downGlyph.setFontSize(12);
return downGlyph; return downGlyph;
} }
public static Glyph getAnchorGlyph() {
Glyph anchorGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ANCHOR);
anchorGlyph.getStyleClass().add("anchor-icon");
anchorGlyph.setFontSize(12);
return anchorGlyph;
}
} }

View file

@ -192,7 +192,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException { public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try { try {
String record = "BSMS 1.0\n" + String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) + OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null) +
"\n/0/*,/1/*\n" + "\n/0/*,/1/*\n" +
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress(); wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
outputStream.write(record.getBytes(StandardCharsets.UTF_8)); outputStream.write(record.getBytes(StandardCharsets.UTF_8));

View file

@ -9,7 +9,7 @@ import java.io.InputStream;
public class BlueWalletMultisig extends ColdcardMultisig { public class BlueWalletMultisig extends ColdcardMultisig {
@Override @Override
public String getName() { public String getName() {
return "Blue Wallet Vault Multisig"; return "BlueWallet Vault Multisig";
} }
@Override @Override
@ -21,7 +21,7 @@ public class BlueWalletMultisig extends ColdcardMultisig {
public Wallet importWallet(InputStream inputStream, String password) throws ImportException { public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = super.importWallet(inputStream, password); Wallet wallet = super.importWallet(inputStream, password);
for(Keystore keystore : wallet.getKeystores()) { for(Keystore keystore : wallet.getKeystores()) {
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet")); keystore.setLabel(keystore.getLabel().replace("Coldcard", "BlueWallet"));
keystore.setWalletModel(WalletModel.BLUE_WALLET); keystore.setWalletModel(WalletModel.BLUE_WALLET);
} }
@ -30,12 +30,12 @@ public class BlueWalletMultisig extends ColdcardMultisig {
@Override @Override
public String getWalletImportDescription() { public String getWalletImportDescription() {
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet."; return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your BlueWallet Vault wallet.";
} }
@Override @Override
public String getWalletExportDescription() { public String getWalletExportDescription() {
return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature."; return "Export file that can be read by BlueWallet using the Add Wallet > Vault > Import wallet feature.";
} }
@Override @Override

View file

@ -2,10 +2,12 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*; import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.Mode; import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.control.QRDensity; import com.sparrowwallet.sparrow.control.QRDensity;
import com.sparrowwallet.sparrow.control.WebcamResolution;
import com.sparrowwallet.sparrow.net.*; import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection; import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy; import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
@ -51,15 +53,19 @@ public class Config {
private boolean showDeprecatedImportExport = false; private boolean showDeprecatedImportExport = false;
private boolean signBsmsExports = false; private boolean signBsmsExports = false;
private boolean preventSleep = false; private boolean preventSleep = false;
private Boolean connectToBroadcast;
private Boolean connectToResolve;
private Boolean suggestSendToMany;
private List<File> recentWalletFiles; private List<File> recentWalletFiles;
private Integer keyDerivationPeriod; private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS; private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS; private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
private QRDensity qrDensity; private QRDensity qrDensity;
private Boolean hdCapture; private WebcamResolution webcamResolution;
private boolean mirrorCapture = true; private boolean mirrorCapture = true;
private boolean useZbar = true; private boolean useZbar = true;
private String webcamDevice; private String webcamDevice;
private String webcamDeviceId;
private ServerType serverType; private ServerType serverType;
private Server publicElectrumServer; private Server publicElectrumServer;
private Server coreServer; private Server coreServer;
@ -68,6 +74,7 @@ public class Config {
private File coreDataDir; private File coreDataDir;
private String coreAuth; private String coreAuth;
private boolean useLegacyCoreWallet; private boolean useLegacyCoreWallet;
private boolean legacyServer;
private Server electrumServer; private Server electrumServer;
private List<Server> recentElectrumServers; private List<Server> recentElectrumServers;
private File electrumServerCert; private File electrumServerCert;
@ -78,6 +85,7 @@ public class Config {
private int maxPageSize = DEFAULT_PAGE_SIZE; private int maxPageSize = DEFAULT_PAGE_SIZE;
private boolean usePayNym; private boolean usePayNym;
private boolean mempoolFullRbf; private boolean mempoolFullRbf;
private double minRelayFeeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
private Double appWidth; private Double appWidth;
private Double appHeight; private Double appHeight;
@ -346,6 +354,34 @@ public class Config {
public void setPreventSleep(boolean preventSleep) { public void setPreventSleep(boolean preventSleep) {
this.preventSleep = preventSleep; this.preventSleep = preventSleep;
flush();
}
public Boolean getConnectToBroadcast() {
return connectToBroadcast;
}
public void setConnectToBroadcast(Boolean connectToBroadcast) {
this.connectToBroadcast = connectToBroadcast;
flush();
}
public Boolean getConnectToResolve() {
return connectToResolve;
}
public void setConnectToResolve(Boolean connectToResolve) {
this.connectToResolve = connectToResolve;
flush();
}
public Boolean getSuggestSendToMany() {
return suggestSendToMany;
}
public void setSuggestSendToMany(Boolean suggestSendToMany) {
this.suggestSendToMany = suggestSendToMany;
flush();
} }
public List<File> getRecentWalletFiles() { public List<File> getRecentWalletFiles() {
@ -383,16 +419,12 @@ public class Config {
flush(); flush();
} }
public Boolean getHdCapture() { public WebcamResolution getWebcamResolution() {
return hdCapture; return webcamResolution;
} }
public Boolean isHdCapture() { public void setWebcamResolution(WebcamResolution webcamResolution) {
return hdCapture != null && hdCapture; this.webcamResolution = webcamResolution;
}
public void setHdCapture(Boolean hdCapture) {
this.hdCapture = hdCapture;
flush(); flush();
} }
@ -418,6 +450,15 @@ public class Config {
flush(); flush();
} }
public String getWebcamDeviceId() {
return webcamDeviceId;
}
public void setWebcamDeviceId(String webcamDeviceId) {
this.webcamDeviceId = webcamDeviceId;
flush();
}
public ServerType getServerType() { public ServerType getServerType() {
return serverType; return serverType;
} }
@ -552,6 +593,15 @@ public class Config {
flush(); flush();
} }
public boolean isLegacyServer() {
return legacyServer;
}
public void setLegacyServer(boolean legacyServer) {
this.legacyServer = legacyServer;
flush();
}
public Server getElectrumServer() { public Server getElectrumServer() {
return electrumServer; return electrumServer;
} }
@ -670,6 +720,14 @@ public class Config {
flush(); flush();
} }
public double getMinRelayFeeRate() {
return minRelayFeeRate;
}
public void setMinRelayFeeRate(double minRelayFeeRate) {
this.minRelayFeeRate = minRelayFeeRate;
}
public Double getAppWidth() { public Double getAppWidth() {
return appWidth; return appWidth;
} }

View file

@ -1,9 +1,12 @@
package com.sparrowwallet.sparrow.io; package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -92,7 +95,7 @@ public class Descriptor implements WalletImport, WalletExport {
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray()); InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
try { try {
return PdfUtils.getOutputDescriptor(firstClone).toWallet(); return ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet());
} catch(Exception e) { } catch(Exception e) {
//ignore //ignore
} }
@ -100,7 +103,7 @@ public class Descriptor implements WalletImport, WalletExport {
List<String> paragraphs = getParagraphs(secondClone); List<String> paragraphs = getParagraphs(secondClone);
for(String paragraph : paragraphs) { for(String paragraph : paragraphs) {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
return descriptor.toWallet(); return ensureKeyDerivations(descriptor.toWallet());
} }
throw new ImportException("Could not find an output descriptor in the file"); throw new ImportException("Could not find an output descriptor in the file");
@ -116,24 +119,34 @@ public class Descriptor implements WalletImport, WalletExport {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) { for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
if(line.isEmpty()) { if(line.isEmpty()) {
if(paragraph.length() > 0) { if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString()); paragraphs.add(paragraph.toString());
paragraph.setLength(0); paragraph.setLength(0);
} }
} else if(line.startsWith("#")) { } else if(line.startsWith("#")) {
continue; continue;
} else { } else {
paragraph.append(line); paragraph.append(line.replaceFirst("^.+:", "").trim());
} }
} }
if(paragraph.length() > 0) { if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString()); paragraphs.add(paragraph.toString());
} }
return paragraphs; return paragraphs;
} }
private static Wallet ensureKeyDerivations(Wallet wallet) {
for(Keystore keystore : wallet.getKeystores()) {
if(keystore.getKeyDerivation().getMasterFingerprint() == null || keystore.getKeyDerivation().getDerivationPath() == null) {
keystore.setKeyDerivation(new KeyDerivation(KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT, wallet.getScriptType().getDefaultDerivationPath()));
}
}
return wallet;
}
@Override @Override
public boolean isWalletImportScannable() { public boolean isWalletImportScannable() {
return true; return true;

View file

@ -37,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
try { try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.write("# Electrum Personal Server configuration file fragments\n"); writer.write("# Electrum Personal Server configuration file fragments\n");
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n"); writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
writer.write("# Copy into [master-public-keys] section\n"); writer.write("# Copy into [master-public-keys] section\n");
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
writeWalletXpub(masterWallet, writer); writeWalletXpub(masterWallet, writer);

View file

@ -1,5 +0,0 @@
package com.sparrowwallet.sparrow.io;
public enum FileType {
TEXT, JSON, BINARY, UNKNOWN;
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io; package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.IOUtils;
import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
@ -56,7 +57,7 @@ public class Hwi {
return lark.enumerate().stream().map(Device::fromHardwareClient).toList(); return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
} catch(Throwable e) { } catch(Throwable e) {
log.error("Error enumerating USB devices", e); log.error("Error enumerating USB devices", e);
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e); throw new ImportException(e.getMessage() == null || e.getMessage().isEmpty() ? "Error scanning, check devices are ready" : e.getMessage(), e);
} finally { } finally {
isPromptActive = false; isPromptActive = false;
} }

View file

@ -1,153 +0,0 @@
package com.sparrowwallet.sparrow.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class IOUtils {
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
public static FileType getFileType(File file) {
try {
String type = Files.probeContentType(file.toPath());
if(type == null) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith("txn") || file.getName().toLowerCase(Locale.ROOT).endsWith("psbt")) {
return FileType.TEXT;
}
if(file.exists()) {
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
String line = br.readLine();
if(line != null) {
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
return FileType.TEXT;
} else if(line.startsWith("{")) {
return FileType.JSON;
}
}
}
}
return FileType.BINARY;
} else if (type.equals("application/json")) {
return FileType.JSON;
} else if (type.startsWith("text")) {
return FileType.TEXT;
}
} catch (IOException e) {
//ignore
}
return FileType.UNKNOWN;
}
/**
* List directory contents for a resource folder. Not recursive.
* This is basically a brute-force implementation.
* Works for regular files, JARs and Java modules.
*
* @param clazz Any java class that lives in the same place as the resources you want.
* @param path Should end with "/", but not start with one.
* @return Just the name of each member item, not the full paths.
* @throws URISyntaxException
* @throws IOException
*/
public static String[] getResourceListing(Class clazz, String path) throws URISyntaxException, IOException {
URL dirURL = clazz.getClassLoader().getResource(path);
if(dirURL != null && dirURL.getProtocol().equals("file")) {
/* A file path: easy enough */
return new File(dirURL.toURI()).list();
}
if(dirURL == null) {
/*
* In case of a jar file, we can't actually find a directory.
* Have to assume the same jar as clazz.
*/
String me = clazz.getName().replace(".", "/")+".class";
dirURL = clazz.getClassLoader().getResource(me);
}
if(dirURL.getProtocol().equals("jar")) {
/* A JAR path */
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
while(entries.hasMoreElements()) {
String name = entries.nextElement().getName();
if(name.startsWith(path)) { //filter according to the path
String entry = name.substring(path.length());
int checkSubdir = entry.indexOf("/");
if (checkSubdir >= 0) {
// if it is a subdirectory, we just return the directory name
entry = entry.substring(0, checkSubdir);
}
if(!entry.isEmpty()) {
result.add(entry);
}
}
}
return result.toArray(new String[result.size()]);
}
if(dirURL.getProtocol().equals("jrt")) {
java.nio.file.FileSystem jrtFs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap());
Path resourcePath = jrtFs.getPath("modules/com.sparrowwallet.sparrow", path);
return Files.list(resourcePath).map(filePath -> filePath.getFileName().toString()).toArray(String[]::new);
}
throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
}
public static boolean deleteDirectory(File directory) {
try {
Files.walk(directory.toPath())
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
} catch(IOException e) {
return false;
}
return true;
}
public static boolean secureDelete(File file) {
if(file.exists()) {
long length = file.length();
SecureRandom random = new SecureRandom();
byte[] data = new byte[1024*1024];
random.nextBytes(data);
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
raf.seek(0);
raf.getFilePointer();
int pos = 0;
while(pos < length) {
raf.write(data);
pos += data.length;
}
} catch(IOException e) {
log.warn("Error overwriting file for deletion: " + file.getName(), e);
}
return file.delete();
}
return false;
}
}

View file

@ -2,6 +2,8 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*; import com.google.gson.*;
import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.FileType;
import com.sparrowwallet.drongo.IOUtils;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;

View file

@ -684,7 +684,7 @@ public class Storage {
public static Executor getSingleThreadedExecutor() { public static Executor getSingleThreadedExecutor() {
if(singleThreadedExecutor == null) { if(singleThreadedExecutor == null) {
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build(); BasicThreadFactory factory = BasicThreadFactory.builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory); singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
} }

View file

@ -1,33 +1,43 @@
package com.sparrowwallet.sparrow.io; package com.sparrowwallet.sparrow.io;
import com.csvreader.CsvReader; import com.csvreader.CsvReader;
import com.google.gson.Gson; import com.google.gson.*;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent; import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent; import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent; import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.*; import com.sparrowwallet.sparrow.wallet.*;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WalletLabels implements WalletImport, WalletExport { public class WalletLabels implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class); private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
private static final long ONE_DAY = 24*60*60*1000L;
private final List<WalletForm> walletForms; private final List<WalletForm> walletForms;
public WalletLabels() {
this.walletForms = Collections.emptyList();
}
public WalletLabels(List<WalletForm> walletForms) { public WalletLabels(List<WalletForm> walletForms) {
this.walletForms = walletForms; this.walletForms = walletForms;
} }
@ -50,8 +60,9 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override @Override
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException { public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>(); List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets(); Map<Date, Double> fiatRates = getFiatRates(walletForms);
for(Wallet exportWallet : allWallets) { for(WalletForm exportWalletForm : walletForms) {
Wallet exportWallet = exportWalletForm.getWallet();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet); OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
String origin = outputDescriptor.toString(true, false, false); String origin = outputDescriptor.toString(true, false, false);
@ -61,34 +72,43 @@ public class WalletLabels implements WalletImport, WalletExport {
} }
} }
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) { Set<Sha256Hash> confirmingTxs = new HashSet<>();
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) { WalletTransactionsEntry walletTransactionsEntry = exportWalletForm.getWalletTransactionsEntry();
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null)); for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
BlockTransaction blkTx = txEntry.getBlockTransaction();
labels.add(new TransactionLabel(blkTx.getHashAsString(), blkTx.getLabel(), origin,
txEntry.isConfirming() ? null : blkTx.getHeight(), blkTx.getDate(),
getFee(walletTransactionsEntry.getWallet(), blkTx), txEntry.getValue(),
getFiatValue(blkTx.getDate(), Transaction.SATOSHIS_PER_BITCOIN, fiatRates)));
if(txEntry.isConfirming()) {
confirmingTxs.add(blkTx.getHash());
} }
} }
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) { for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) { labels.add(new AddressLabel(addressNode.getAddress().toString(), addressNode.getLabel(), origin, addressNode.getDerivationPath().substring(1),
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null)); addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
} .filter(ref -> !confirmingTxs.contains(ref.getHash())).map(BlockTransactionHash::getHeight).toList()));
} }
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : exportWallet.getWalletTxos().entrySet()) {
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true"); BlockTransactionHashIndex txo = txoEntry.getKey();
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) { WalletNode addressNode = txoEntry.getValue();
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable)); Boolean spendable = (txo.isSpent() ? null : txo.getStatus() != Status.FROZEN);
} else if(!txo.isSpent()) { labels.add(new InputOutputLabel(Type.output, txo.toString(), txo.getLabel(), origin, spendable, addressNode.getDerivationPath().substring(1), txo.getValue(),
labels.add(new Label(Type.output, txo.toString(), null, null, spendable)); confirmingTxs.contains(txo.getHash()) ? null : txo.getHeight(), txo.getDate(), getFiatValue(txo, fiatRates)));
}
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) { if(txo.isSpent()) {
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null)); BlockTransactionHashIndex txi = txo.getSpentBy();
labels.add(new InputOutputLabel(Type.input, txi.toString(), txi.getLabel(), origin, null, addressNode.getDerivationPath().substring(1), txi.getValue(),
confirmingTxs.contains(txi.getHash()) ? null : txi.getHeight(), txi.getDate(), getFiatValue(txi, fiatRates)));
} }
} }
} }
try { try {
Gson gson = new Gson(); Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new GsonUTCDateAdapter()).create();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for(Label label : labels) { for(Label label : labels) {
@ -114,7 +134,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override @Override
public boolean isWalletExportScannable() { public boolean isWalletExportScannable() {
return false; return true;
} }
@Override @Override
@ -185,7 +205,7 @@ public class WalletLabels implements WalletImport, WalletExport {
} }
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet); OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
String origin = outputDescriptor.toString(true, false, false); Origin origin = Origin.fromOutputDescriptor(outputDescriptor);
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren(); List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
List<Entry> addressEntries = new ArrayList<>(); List<Entry> addressEntries = new ArrayList<>();
@ -194,7 +214,7 @@ public class WalletLabels implements WalletImport, WalletExport {
List<Entry> utxoEntries = walletForm.getWalletUtxosEntry().getChildren(); List<Entry> utxoEntries = walletForm.getWalletUtxosEntry().getChildren();
for(Label label : labels) { for(Label label : labels) {
if(label.origin != null && !label.origin.equals(origin)) { if(label.origin != null && !Origin.fromString(label.origin).equals(origin)) {
continue; continue;
} }
@ -247,11 +267,11 @@ public class WalletLabels implements WalletImport, WalletExport {
addChangedEntry(changedWalletEntries, txioEntry); addChangedEntry(changedWalletEntries, txioEntry);
} }
if(label.type == Type.output && !reference.isSpent()) { if(label.type == Type.output && !reference.isSpent() && label.spendable != null) {
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) { if(!label.spendable && reference.getStatus() != Status.FROZEN) {
reference.setStatus(Status.FROZEN); reference.setStatus(Status.FROZEN);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry); addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) { } else if(label.spendable && reference.getStatus() == Status.FROZEN) {
reference.setStatus(null); reference.setStatus(null);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry); addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
} }
@ -316,7 +336,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override @Override
public boolean isWalletImportScannable() { public boolean isWalletImportScannable() {
return false; return true;
} }
@Override @Override
@ -324,12 +344,99 @@ public class WalletLabels implements WalletImport, WalletExport {
return true; return true;
} }
private Long getFee(Wallet wallet, BlockTransaction blockTransaction) {
long fee = 0L;
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
if(txInput.isCoinBase()) {
return 0L;
}
BlockTransaction inputTx = wallet.getWalletTransaction(txInput.getOutpoint().getHash());
if(inputTx == null || inputTx.getTransaction().getOutputs().size() <= txInput.getOutpoint().getIndex()) {
return null;
}
TransactionOutput spentOutput = inputTx.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
fee += spentOutput.getValue();
}
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
fee -= txOutput.getValue();
}
return fee;
}
private Map<Date, Double> getFiatRates(List<WalletForm> walletForms) {
ExchangeSource exchangeSource = getExchangeSource();
Currency fiatCurrency = getFiatCurrency();
Map<Date, Double> fiatRates = new HashMap<>();
if(fiatCurrency != null) {
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
for(WalletForm walletForm : walletForms) {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
if(!walletTransactionsEntry.getChildren().isEmpty()) {
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
.filter(Objects::nonNull)
.collect(Collectors.summarizingLong(Date::getTime));
min = Math.min(min, stats.getMin());
max = Math.max(max, stats.getMax());
}
}
if(max > min) {
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(min - ONE_DAY), new Date(max));
}
}
return fiatRates;
}
private static ExchangeSource getExchangeSource() {
return Config.get().getExchangeSource() == null ? ExchangeSource.COINGECKO : Config.get().getExchangeSource();
}
private static Currency getFiatCurrency() {
return getExchangeSource() == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency();
}
private Map<Currency, BigDecimal> getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
return getFiatValue(txEntry.getBlockTransaction().getDate(), txEntry.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(BlockTransactionHashIndex ref, Map<Date, Double> fiatRates) {
return getFiatValue(ref.getDate(), ref.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(Date date, long value, Map<Date, Double> fiatRates) {
Currency fiatCurrency = getFiatCurrency();
if(fiatCurrency != null) {
Double dayRate = null;
if(date == null) {
if(AppServices.getFiatCurrencyExchangeRate() != null) {
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
}
} else {
dayRate = fiatRates.get(DateUtils.truncate(date, Calendar.DAY_OF_MONTH));
}
if(dayRate != null) {
BigDecimal fiatValue = BigDecimal.valueOf(dayRate * value / Transaction.SATOSHIS_PER_BITCOIN);
return Map.of(fiatCurrency, fiatValue.setScale(fiatCurrency.getDefaultFractionDigits(), RoundingMode.HALF_UP));
}
}
return null;
}
private enum Type { private enum Type {
tx, addr, pubkey, input, output, xpub tx, addr, pubkey, input, output, xpub
} }
private static class Label { private static class Label {
public Label(Type type, String ref, String label, String origin, String spendable) { public Label(Type type, String ref, String label, String origin, Boolean spendable) {
this.type = type; this.type = type;
this.ref = ref; this.ref = ref;
this.label = label; this.label = label;
@ -341,6 +448,119 @@ public class WalletLabels implements WalletImport, WalletExport {
String ref; String ref;
String label; String label;
String origin; String origin;
String spendable; Boolean spendable;
}
private static class TransactionLabel extends Label {
public TransactionLabel(String ref, String label, String origin, Integer height, Date time, Long fee, Long value, Map<Currency, BigDecimal> rate) {
super(Type.tx, ref, label, origin, null);
this.height = height;
this.time = time;
this.fee = fee;
this.value = value;
this.rate = rate;
}
Integer height;
Date time;
Long fee;
Long value;
Map<Currency, BigDecimal> rate;
}
private static class AddressLabel extends Label {
public AddressLabel(String ref, String label, String origin, String keypath, List<Integer> heights) {
super(Type.addr, ref, label, origin, null);
this.keypath = keypath;
this.heights = heights;
}
String keypath;
List<Integer> heights;
}
private static class InputOutputLabel extends Label {
public InputOutputLabel(Type type, String ref, String label, String origin, Boolean spendable, String keypath, Long value, Integer height, Date time, Map<Currency, BigDecimal> fmv) {
super(type, ref, label, origin, spendable);
this.keypath = keypath;
this.value = value;
this.height = height;
this.time = time;
this.fmv = fmv;
}
String keypath;
Long value;
Integer height;
Date time;
Map<Currency, BigDecimal> fmv;
}
public static class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
private final DateFormat dateFormat;
public GsonUTCDateAdapter() {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
public JsonElement serialize(Date src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(dateFormat.format(src));
}
@Override
public Date deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return dateFormat.parse(json.getAsString());
} catch (ParseException e) {
throw new JsonParseException(e);
}
}
}
private static class Origin {
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)?\\]");
private ScriptType scriptType;
private Set<KeyDerivation> keyDerivations;
@Override
public final boolean equals(Object o) {
if(this == o) {
return true;
}
if(!(o instanceof Origin origin)) {
return false;
}
return scriptType == origin.scriptType && keyDerivations.equals(origin.keyDerivations);
}
@Override
public int hashCode() {
int result = Objects.hashCode(scriptType);
result = 31 * result + keyDerivations.hashCode();
return result;
}
public static Origin fromOutputDescriptor(OutputDescriptor outputDescriptor) {
Origin origin = new Origin();
origin.scriptType = outputDescriptor.getScriptType();
origin.keyDerivations = new HashSet<>(outputDescriptor.getExtendedPublicKeysMap().values());
return origin;
}
public static Origin fromString(String strOrigin) {
Origin origin = new Origin();
origin.scriptType = ScriptType.fromDescriptor(strOrigin);
origin.keyDerivations = new HashSet<>();
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(strOrigin);
while(keyOriginMatcher.find()) {
byte[] masterFingerprintBytes = keyOriginMatcher.group(1) != null ? Utils.hexToBytes(keyOriginMatcher.group(1)) : new byte[4];
origin.keyDerivations.add(new KeyDerivation(Utils.bytesToHex(masterFingerprintBytes), KeyDerivation.writePath(KeyDerivation.parsePath(keyOriginMatcher.group(2)))));
}
return origin;
}
} }
} }

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