Compare commits

..
78 Commits
Author SHA1 Message Date
Craig Raw 4cdffec060 fix fiat amount update on transaction update 2020-09-01 13:13:38 +02:00
Craig Raw ed9bc882b7 minor ui fix, safe unsubscribe 2020-09-01 11:33:18 +02:00
Craig Raw 5054ab9564 disable sign button if signing wallet is closed 2020-09-01 09:50:44 +02:00
Craig Raw 7751f940f2 display output send type better, handle consolidation sends 2020-09-01 08:58:47 +02:00
Craig Raw 3f68ca43c2 dont show uninitialized devices (trezor t) 2020-09-01 08:04:58 +02:00
Craig Raw 68239e9472 improve device detection and display 2020-08-31 15:55:36 +02:00
Craig Raw 10edf31e13 dont open the same wallet twice 2020-08-31 11:47:06 +02:00
Craig Raw 61c3c4a3ac use minimum fee rate found for higher block targets if present 2020-08-31 11:28:03 +02:00
Craig Raw 5d6bb5e4f9 show better descriptions on outputs based on signing wallet 2020-08-31 11:22:15 +02:00
Craig Raw b808222faa deduplicate signed keystores on signature 2020-08-31 10:14:48 +02:00
Craig Raw 4d7d51f3f8 dont show status server banner (but show height in tooltip) 2020-08-30 13:47:31 +02:00
Craig Raw 546076ea48 avoid displaying any server response in error dialogs 2020-08-30 13:29:42 +02:00
Craig Raw 2bebb0925e wallet save fixes 2020-08-30 11:24:49 +02:00
Craig Raw 47855228d3 wallet load order, dont clear history on trivial changes 2020-08-30 11:15:45 +02:00
Craig Raw a6e41e3f9d fix icon on app installer 2020-08-29 20:44:50 +02:00
Craig Raw 04722204a6 set app name on X11 2020-08-29 20:06:50 +02:00
Craig Raw 452afe4731 tweak naming 2020-08-29 18:04:27 +02:00
Craig Raw af36d7b5a5 update readme for initial release 2020-08-29 17:56:09 +02:00
Craig Raw 7e225e794f fix commit 2020-08-28 19:41:32 +02:00
Craig Raw ffce18919f Merge branch 'master' of github.com:sparrowwallet/sparrow into master 2020-08-28 19:39:34 +02:00
Craig Raw ddcabf721e dont poll when signing 2020-08-28 19:38:55 +02:00
Craig Raw 7eb4b9c61d linux build fixes 2020-08-28 16:02:08 +02:00
Craig Raw 6dca03f8b4 build tweak #2 2020-08-28 15:34:35 +02:00
Craig Raw 135c1d1719 build tweak 2020-08-28 15:29:25 +02:00
Craig Raw c9a4fb4ec4 license 2020-08-28 15:20:31 +02:00
Craig Raw 06957b12e6 build ubuntu 2020-08-28 14:42:38 +02:00
Craig Raw d1fbe94d90 welcome page tweak 2020-08-28 14:41:26 +02:00
Craig Raw 1fea119173 shorten button width 2020-08-28 13:31:27 +02:00
Craig Raw 1ac264600a set linux path explicitly 2020-08-28 12:17:47 +02:00
Craig Raw 00db59c44f fix pixel stretching issue 2020-08-28 09:49:26 +02:00
Craig Raw 2c27f702bf add import 2020-08-27 17:41:17 +02:00
Craig Raw 3c2dfed96d locale and other windows related fixes 2020-08-27 17:16:20 +02:00
Craig Raw eb23cc8e61 osx only run options 2020-08-27 15:23:42 +02:00
Craig Raw fa0c74f06b set monospace font explicitly, add stage icons 2020-08-27 13:11:23 +02:00
Craig Raw ba017b2fcb windows fixes 2020-08-25 19:30:19 +02:00
Craig Raw 0a094332d2 fix folder name 2020-08-25 17:24:22 +02:00
Craig Raw 2cca6049be create installers, fix about border 2020-08-25 17:18:45 +02:00
Craig Raw 5be1f066df use matrix 2020-08-25 16:20:45 +02:00
Craig Raw d519c033ac bump version, cross platform builds 2020-08-25 16:10:56 +02:00
Craig Raw f8d15f73f6 platform specific changes 2020-08-25 15:31:04 +02:00
Craig Raw 350bddac84 java versioning 2020-08-25 12:17:33 +02:00
Craig Raw d1326cb621 update java point release 2020-08-25 09:17:28 +02:00
Craig Raw ae0e5894ca fix submodule url 2020-08-25 07:50:02 +02:00
Craig Raw 1a931c72bd checkout submodules 2020-08-24 17:46:28 +02:00
Craig Raw 92db6d9396 remove newline 2020-08-24 15:54:54 +02:00
Craig Raw 5fd59cc68a fix event name #2 2020-08-24 15:40:44 +02:00
Craig Raw ea9a310b22 fix event name 2020-08-24 15:39:31 +02:00
Craig Raw ee22d4a81d osx github action initial commit 2020-08-24 15:37:35 +02:00
Craig Raw c2fb6bcfee build, versioning and cross-platform about dlg 2020-08-24 14:52:05 +02:00
Craig Raw 8cde670efd log all errors to error log 2020-08-24 12:54:11 +02:00
Craig Raw f95cb67912 add coldcard single sig wallet import 2020-08-24 12:05:32 +02:00
Craig Raw 750f6483cb show alternative xpub values as tooltips 2020-08-24 10:12:16 +02:00
Craig Raw 53a590140d spend utxo improvements 2020-08-23 15:47:43 +02:00
Craig Raw 189ac75467 set null date on unconfirmed txes, dont let external utxos show as spendable 2020-08-22 17:20:43 +02:00
Craig Raw fa082e892f about dialog, older mac hwi build 2020-08-21 20:58:11 +02:00
Craig Raw c814b8e350 various fixes, better non-electrumx support 2020-08-21 17:16:14 +02:00
Craig Raw 2b48b4cd6e better handling of server connection editing 2020-08-21 10:48:41 +02:00
Craig Raw d49f85fe5b more import fixes 2020-08-21 10:08:43 +02:00
Craig Raw af525797ff improvements to importing and exporting 2020-08-21 09:59:11 +02:00
Craig Raw e9e10de266 export file naming with correct extensions 2020-08-20 18:20:42 +02:00
Craig Raw ab5fdef919 display full derivation path where possible 2020-08-20 17:59:58 +02:00
Craig Raw f723d20c9a disable absolute locktime if sequence disabled 2020-08-20 17:40:56 +02:00
Craig Raw 03c488cf4d add warnings to block time, set current height, close once 2020-08-20 17:30:39 +02:00
Craig Raw ca64a1d307 scan qr to bitcoin uri 2020-08-20 15:26:59 +02:00
Craig Raw 4c8f7bb711 clear fiat amounts 2020-08-20 12:30:21 +02:00
Craig Raw 77dd8ca5d7 unregister closed tab and dialog subscribers, fix connection error reset behaviour 2020-08-20 11:25:34 +02:00
Craig Raw d5aba35184 make controller and tab data share data sources 2020-08-19 15:44:40 +02:00
Craig Raw 30e2ab5e2a fill tx labels on send 2020-08-19 08:40:06 +02:00
Craig Raw e93fcf7fbd show fiat fee amount 2020-08-18 12:54:43 +02:00
Craig Raw 198672a06e further keystore improvements 2020-08-18 11:50:27 +02:00
Craig Raw 133d771c09 keystore buttons label tooltips 2020-08-18 11:02:45 +02:00
Craig Raw 9a80065605 connection toggle tooltip 2020-08-18 10:55:07 +02:00
Craig Raw ccf4e98968 ellipsis and incomplete wallet background save fixes 2020-08-18 10:47:32 +02:00
Craig Raw df15129f64 bip39 word list improvements #2 2020-08-18 10:20:56 +02:00
Craig Raw e878648587 bip39 word list usability improvements 2020-08-17 19:42:30 +02:00
Craig Raw 75bf63d936 fix wallet settings update bugs 2020-08-17 17:25:33 +02:00
Craig Raw 304bdd5c48 remove sys err exception logging 2020-08-17 17:00:48 +02:00
Craig Raw e1934c99bc upgrade controlsfx, fix notifications bug 2020-08-17 16:48:31 +02:00
127 changed files with 1975 additions and 487 deletions
+33
View File
@@ -0,0 +1,33 @@
name: Package
on: workflow_dispatch
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 14.0.2
uses: actions/setup-java@v1
with:
java-version: 14.0.2
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build with Gradle
run: ./gradlew jpackage
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: Sparrow Build - ${{ runner.os }}
path: build/jpackage/*
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "drongo"]
path = drongo
url = git@github.com:craigraw/drongo.git
url = git@github.com:sparrowwallet/drongo.git
+174 -16
View File
@@ -1,19 +1,177 @@
Copyright (c) 2020 Craig Raw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+37 -7
View File
@@ -1,11 +1,41 @@
# Sparrow
Bitcoin Wallet & Transaction Editor
# Sparrow Bitcoin Wallet
To clone this project, use `git clone --recursive git@github.com:craigraw/sparrow.git`
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
## Various ways to hex dump a file without spaces:
`xxd -p file | tr -d '\n'`
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
`hexdump -ve '1/1 "%.2x"'`
![Sparrow Wallet](https://sparrowwallet.com/assets/images/control-your-sends.png)
`od -t x1 -An file | tr -d '\n '`
## Building
To clone this project, use `git clone --recursive git@github.com:sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
`./gradlew jpackage`
## Running
If you prefer to run Sparrow directly from source, it can be launched with
`./gradlew run`
Java 14 must be installed.
## Configuration
Sparrow stores it's configuration, log file and wallets in a location appropriate to the operating system:
Platform | Location
-------- | --------
OSX | ~/.sparrow
Linux | ~/.sparrow
Windows | %APPDATA%/Sparrow
## Reporting Issues
Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab above to report an issue. If possible, look in the sparrow.log file in the configuration directory for information helpful in debugging.
## License
Sparrow is licensed under the Apache 2 software licence.
+48 -17
View File
@@ -5,10 +5,15 @@ plugins {
id 'org.beryx.jlink' version '2.17.4'
}
group 'com.sparrowwallet'
version '1.0-SNAPSHOT'
def sparrowVersion = '0.9.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "mac"
}
sourceCompatibility = 1.9
group "com.sparrowwallet"
version "${sparrowVersion}"
repositories {
mavenCentral()
@@ -16,6 +21,11 @@ repositories {
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
javafx {
version = "14"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
@@ -45,7 +55,7 @@ dependencies {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.1' ) {
implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@@ -62,13 +72,22 @@ mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
compileJava {
options.with {
fork = true
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow"])
compilerArgs.addAll(["--add-exports", "org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-exports", "javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"])
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/external").matching {
exclude "$osName/**"
}
}
}
run {
applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
@@ -81,7 +100,13 @@ run {
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
}
jlink {
@@ -111,25 +136,31 @@ jlink {
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
}
addExtraDependencies("javafx")
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "0.6"
skipInstaller = true
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX
imageOptions = []
installerOptions = [
'--file-associations', 'src/main/resources/associations.properties',
]
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu']
installerOptions = ['--file-associations', 'src/main/deploy/associations.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
installerType = "exe"
}
if (org.gradle.internal.os.OperatingSystem.current().macOsX) {
if(os.linux) {
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
}
if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/macosx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macosx/']
imageOptions += ['--icon', 'src/main/deploy/package/mac/sparrow.icns', '--resource-dir', 'src/main/deploy/package/mac/']
installerType = "dmg"
}
}
+1 -1
Submodule drongo updated: 6a2af38b8a...c7e16a29e3
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.6</string>
<string>0.9.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.stage.Stage;
public class AboutController {
private Stage stage;
@FXML
private Label title;
public void initializeView() {
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
}
public void setStage(Stage stage) {
this.stage = stage;
}
public void close(ActionEvent event) {
stage.close();
}
}
@@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
@@ -37,14 +38,18 @@ import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;
import org.controlsfx.control.Notifications;
@@ -83,6 +88,9 @@ public class AppController implements Initializable {
@FXML
private Menu fileMenu;
@FXML
private Menu helpMenu;
@FXML
private MenuItem openTransactionIdItem;
@@ -178,7 +186,8 @@ public class AppController implements Initializable {
}
});
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
//Draggle tabs introduce unwanted movement when selecting between them
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
@@ -187,6 +196,18 @@ public class AppController implements Initializable {
EventManager.get().post(new OpenWalletsEvent(getOpenWallets()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.WALLET).map(tabData -> (WalletTabData)tabData).collect(Collectors.toList());
if(!closedWalletTabs.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
}
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData)tabData).collect(Collectors.toList());
if(!closedTransactionTabs.isEmpty()) {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage)tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
@@ -234,6 +255,9 @@ public class AppController implements Initializable {
});
onlineProperty.bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(this::setServerToggleTooltip);
});
Config config = Config.get();
connectionService = createConnectionService();
@@ -266,10 +290,14 @@ public class AppController implements Initializable {
}
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
connectionService.resetConnection();
changeMode = false;
onlineProperty.setValue(false);
changeMode = true;
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
@@ -312,20 +340,49 @@ public class AppController implements Initializable {
}
private void setOsxApplicationMenu() {
if(org.controlsfx.tools.Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) {
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX) {
MenuToolkit tk = MenuToolkit.toolkit();
MenuItem preferences = new MenuItem("Preferences...");
preferences.setOnAction(this::openPreferences);
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, null), new SeparatorMenuItem(),
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
preferences, new SeparatorMenuItem(),
tk.createHideMenuItem(MainApp.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(MainApp.APP_NAME));
tk.setApplicationMenu(defaultApplicationMenu);
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide"));
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide"));
}
}
public void showAbout(ActionEvent event) {
Stage aboutStage = getAboutStage();
aboutStage.show();
}
private Stage getAboutStage() {
try {
FXMLLoader loader = new FXMLLoader(AppController.class.getResource("about.fxml"));
Parent root = loader.load();
AboutController controller = loader.getController();
Stage stage = new Stage();
stage.setTitle("About " + MainApp.APP_NAME);
stage.initStyle(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? StageStyle.UNDECORATED : StageStyle.DECORATED);
setStageIcon(stage);
stage.setResizable(false);
stage.setScene(new Scene(root));
controller.setStage(stage);
controller.initializeView();
return stage;
} catch(IOException e) {
log.error("Error loading about stage", e);
}
return null;
}
public void openTransactionFromFile(ActionEvent event) {
Stage window = new Stage();
@@ -430,7 +487,8 @@ public class AppController implements Initializable {
transactionReferenceService.setOnFailed(failEvent -> {
Platform.runLater(() -> {
Throwable e = failEvent.getSource().getException();
showErrorDialog("Error fetching transaction", e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
log.error("Error fetching transaction " + txId.toString(), e);
showErrorDialog("Error fetching transaction", "The server returned an error when fetching the transaction. The server response is contained in sparrow.log");
});
});
transactionReferenceService.start();
@@ -467,13 +525,9 @@ public class AppController implements Initializable {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
Transaction transaction = transactionTabData.getTransaction();
//Note the transactionTabData's transaction does not change even once the final tx is extracted, so extract it here if possible
if(transactionTabData.getPsbt() != null && transactionTabData.getPsbt().isFinalized()) {
transaction = transactionTabData.getPsbt().extractTransaction();
}
//Save a transaction if the PSBT is null or finalized (a finalized PSBT is less useful than a broadcastable tx)
boolean saveTx = (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().isFinalized());
//Save a transaction if the PSBT is null or transaction has already been extracted, otherwise save PSBT
//The PSBT's transaction is not altered with transaction extraction, but the extracted transaction is stored in TransactionData
boolean saveTx = (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transaction);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@@ -481,8 +535,10 @@ public class AppController implements Initializable {
String fileName = selectedTab.getText();
if(fileName != null && !fileName.isEmpty()) {
if(transactionTabData.getPsbt() != null && !fileName.endsWith(".psbt")) {
fileName += ".psbt";
if(transactionTabData.getPsbt() != null) {
if(!fileName.endsWith(".psbt")) {
fileName += ".psbt";
}
} else if(!fileName.endsWith(".txn")) {
fileName += ".txn";
}
@@ -516,6 +572,14 @@ public class AppController implements Initializable {
return onlineProperty.get();
}
public void connect() {
serverToggle.selectedProperty().set(true);
}
public void disconnect() {
serverToggle.selectedProperty().set(false);
}
public static BooleanProperty onlineProperty() { return onlineProperty; }
public static Integer getCurrentBlockHeight() {
@@ -550,12 +614,22 @@ public class AppController implements Initializable {
public static void showErrorDialog(String title, String content) {
Alert alert = new Alert(Alert.AlertType.ERROR);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(content);
alert.showAndWait();
}
public static void setStageIcon(Window window) {
Stage stage = (Stage)window;
stage.getIcons().add(new Image(AppController.class.getResourceAsStream("/image/sparrow.png")));
}
public static Font getMonospaceFont() {
return Font.font("Roboto Mono", 13);
}
public void selectTab(Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
@@ -598,6 +672,10 @@ public class AppController implements Initializable {
return FileType.JSON.equals(fileType) || FileType.BINARY.equals(fileType);
}
private void setServerToggleTooltip() {
serverToggle.setTooltip(new Tooltip(isOnline() ? "Connected to " + Config.get().getElectrumServer() + (getCurrentBlockHeight() != null ? " at height " + getCurrentBlockHeight() : "") : "Disconnected"));
}
public void newWallet(ActionEvent event) {
WalletNameDialog dlg = new WalletNameDialog();
Optional<String> walletName = dlg.showAndWait();
@@ -722,6 +800,30 @@ public class AppController implements Initializable {
if(optionalWallet.isPresent()) {
Wallet wallet = optionalWallet.get();
File walletFile = Storage.getWalletFile(wallet.getName());
if(walletFile.exists()) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Existing wallet found");
alert.setHeaderText("Replace existing wallet?");
alert.setContentText("Wallet file " + wallet.getName() + " already exists");
Optional<ButtonType> result = alert.showAndWait();
if(result.isPresent() && result.get() == ButtonType.CANCEL) {
return;
}
//Close existing wallet first if open
for(Iterator<Tab> iter = tabs.getTabs().iterator(); iter.hasNext(); ) {
Tab tab = iter.next();
TabData tabData = (TabData)tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(walletTabData.getStorage().getWalletFile().equals(walletFile)) {
iter.remove();
}
}
}
}
Storage storage = new Storage(walletFile);
Tab tab = addWalletTab(storage, wallet);
tabs.getSelectionModel().select(tab);
@@ -747,14 +849,22 @@ public class AppController implements Initializable {
}
public Tab addWalletTab(Storage storage, Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
if(storage.getWalletFile().equals(walletTabData.getStorage().getWalletFile())) {
return tab;
}
}
}
try {
String name = storage.getWalletFile().getName();
if(name.endsWith(".json")) {
name = name.substring(0, name.lastIndexOf('.'));
}
Tab tab = new Tab(name);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, wallet, storage);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
@@ -766,6 +876,9 @@ public class AppController implements Initializable {
EventManager.get().register(walletForm);
controller.setWalletForm(walletForm);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tabs.getTabs().add(tab);
return tab;
} catch(IOException e) {
@@ -862,29 +975,30 @@ public class AppController implements Initializable {
}
Tab tab = new Tab(tabName);
TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transaction, psbt);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("transaction/transaction.fxml"));
tab.setContent(transactionLoader.load());
TransactionController controller = transactionLoader.getController();
TransactionData transactionData;
if(psbt != null) {
controller.setPSBT(psbt);
transactionData = new TransactionData(name, psbt);
} else if(blockTransaction != null) {
controller.setBlockTransaction(blockTransaction);
transactionData = new TransactionData(name, blockTransaction);
} else {
controller.setTransaction(transaction);
transactionData = new TransactionData(name, transaction);
}
controller.setName(name);
controller.setTransactionData(transactionData);
if(initialView != null) {
controller.setInitialView(initialView, initialIndex);
}
controller.initializeView();
TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transactionData);
tab.setUserData(tabData);
tabs.getTabs().add(tab);
return tab;
@@ -929,7 +1043,7 @@ public class AppController implements Initializable {
TransactionTabSelectedEvent txTabEvent = (TransactionTabSelectedEvent)event;
TransactionTabData transactionTabData = txTabEvent.getTransactionTabData();
saveTransaction.setDisable(false);
saveTransaction.setText("Save " + (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().isFinalized() ? "Transaction..." : "PSBT..."));
saveTransaction.setText("Save " + (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction() ? "Transaction..." : "PSBT..."));
exportWallet.setDisable(true);
showTxHex.setDisable(false);
} else if(event instanceof WalletTabSelectedEvent) {
@@ -943,12 +1057,12 @@ public class AppController implements Initializable {
}
@Subscribe
public void psbtFinalizedEvent(PSBTFinalizedEvent event) {
public void transactionExtractedEvent(TransactionExtractedEvent event) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData instanceof TransactionTabData) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), event.getPsbt().getTransaction().bitcoinSerialize())) {
if(transactionTabData.getTransaction() == event.getFinalTransaction()) {
saveTransaction.setText("Save Transaction...");
}
}
@@ -966,7 +1080,7 @@ public class AppController implements Initializable {
String text;
if(event.getBlockTransactions().size() == 1) {
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
if(blockTransaction.getHeight() == 0) {
if(blockTransaction.getHeight() <= 0) {
text = "New mempool transaction: ";
} else {
int confirmations = blockTransaction.getConfirmations(getCurrentBlockHeight());
@@ -982,11 +1096,11 @@ public class AppController implements Initializable {
text += event.getValueAsText(event.getTotalValue());
} else {
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
} else if(event.getTotalMempoolValue() > 0) {
text = "New mempool transactions: " + event.getValueAsText(event.getTotalMempoolValue());
text = "New mempool transactions: " + event.getValueAsText(event.getTotalMempoolValue()) + " total";
} else {
text = "New transactions: " + event.getValueAsText(event.getTotalValue());
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total";
}
}
@@ -1002,11 +1116,16 @@ public class AppController implements Initializable {
.title("Sparrow - " + event.getWallet().getName())
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(5))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.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(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
}
notificationBuilder.show();
}
}
@@ -1081,7 +1200,7 @@ public class AppController implements Initializable {
currentBlockHeight = event.getBlockHeight();
targetBlockFeeRates = event.getTargetBlockFeeRates();
String banner = event.getServerBanner();
String status = "Connected: " + (banner == null ? "Server" : banner.split(System.lineSeparator(), 2)[0]) + " at height " + event.getBlockHeight();
String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight();
EventManager.get().post(new StatusEvent(status));
}
@@ -1095,6 +1214,7 @@ public class AppController implements Initializable {
@Subscribe
public void newBlock(NewBlockEvent event) {
currentBlockHeight = event.getHeight();
setServerToggleTooltip();
String status = "Updating to new block height " + event.getHeight();
EventManager.get().post(new StatusEvent(status));
}
@@ -1191,4 +1311,14 @@ public class AppController implements Initializable {
public void requestQRScan(RequestQRScanEvent event) {
openTransactionFromQR(null);
}
@Subscribe
public void requestConnect(RequestConnectEvent event) {
connect();
}
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
disconnect();
}
}
@@ -4,6 +4,8 @@ import com.sparrowwallet.sparrow.control.WelcomeDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.FileType;
import com.sparrowwallet.sparrow.io.IOUtils;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import javafx.application.Application;
@@ -12,24 +14,37 @@ import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.GlyphFontRegistry;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "0.9.2";
private Stage mainStage;
@Override
public void init() throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
super.init();
}
@Override
public void start(Stage stage) throws Exception {
this.mainStage = stage;
GlyphFontRegistry.register(new FontAwesome5());
GlyphFontRegistry.register(new FontAwesome5Brands());
Font.loadFont(AppController.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
boolean createNewWallet = false;
Mode mode = Config.get().getMode();
@@ -72,7 +87,14 @@ public class MainApp extends Application {
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null) {
for(File walletFile : recentWalletFiles) {
//Resort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList());
Collections.reverse(encryptedWalletFiles);
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
sortedWalletFiles.removeAll(encryptedWalletFiles);
sortedWalletFiles.addAll(encryptedWalletFiles);
for(File walletFile : sortedWalletFiles) {
if(walletFile.exists()) {
Platform.runLater(() -> appController.openWalletFile(walletFile));
}
@@ -86,6 +108,6 @@ public class MainApp extends Application {
}
public static void main(String[] args) {
launch(args);
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, args);
}
}
@@ -0,0 +1,11 @@
package com.sparrowwallet.sparrow;
import javafx.application.Preloader;
import javafx.stage.Stage;
public class MainAppPreloader extends Preloader {
@Override
public void start(Stage stage) {
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
}
}
@@ -2,26 +2,25 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.transaction.TransactionData;
public class TransactionTabData extends TabData {
private final Transaction transaction;
private final PSBT psbt;
private final TransactionData transactionData;
public TransactionTabData(TabType type, Transaction transaction) {
this(type, transaction, null);
public TransactionTabData(TabType type, TransactionData transactionData) {
super(type);
this.transactionData = transactionData;
}
public TransactionTabData(TabType type, Transaction transaction, PSBT psbt) {
super(type);
this.transaction = transaction;
this.psbt = psbt;
public TransactionData getTransactionData() {
return transactionData;
}
public Transaction getTransaction() {
return transaction;
return transactionData.getTransaction();
}
public PSBT getPsbt() {
return psbt;
return transactionData.getPsbt();
}
}
@@ -1,34 +1,26 @@
package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.WalletForm;
public class WalletTabData extends TabData {
private Wallet wallet;
private final Storage storage;
private final WalletForm walletForm;
public WalletTabData(TabType type, Wallet wallet, Storage storage) {
public WalletTabData(TabType type, WalletForm walletForm) {
super(type);
this.wallet = wallet;
this.storage = storage;
this.walletForm = walletForm;
}
EventManager.get().register(this);
public WalletForm getWalletForm() {
return walletForm;
}
public Wallet getWallet() {
return wallet;
return walletForm.getWallet();
}
public Storage getStorage() {
return storage;
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletFile().equals(storage.getWalletFile())) {
wallet = event.getWallet();
}
return walletForm.getStorage();
}
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
@@ -10,7 +11,6 @@ import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Font;
import java.util.List;
import java.util.Optional;
@@ -33,7 +33,7 @@ public class AddressTreeTable extends CoinTreeTable {
getColumns().add(addressCol);
if(address != null) {
addressCol.setMinWidth(TextUtils.computeTextWidth(Font.font("Courier"), address, 0.0));
addressCol.setMinWidth(TextUtils.computeTextWidth(AppController.getMonospaceFont(), address, 0.0));
}
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
@@ -39,6 +39,7 @@ public class BalanceChart extends LineChart<Number, Number> {
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
.map(entry -> (TransactionEntry)entry)
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
.collect(Collectors.toList());
@@ -83,7 +84,7 @@ public class BalanceChart extends LineChart<Number, Number> {
XYChart.Data<Number, Number> data = balanceSeries.getData().get(i);
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
if(data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
if(transactionEntry.getBlockTransaction().getDate() != null && data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
symbol.getStyleClass().add("selected");
selectedEntry = transactionEntry;
}
@@ -58,7 +58,7 @@ public class CoinLabel extends CopyableLabel {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats";
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
BitcoinUnit unit = bitcoinUnit;
@@ -4,13 +4,15 @@ import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextInputControl;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class CoinTextFormatter extends TextFormatter<String> {
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########");
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public CoinTextFormatter() {
super(new CoinFilter());
@@ -54,7 +54,7 @@ public class ConfirmationProgressIndicator extends StackPane {
arcLengthTimeline.getKeyFrames().add(arcLengthFrame);
sequence.getChildren().add(arcLengthTimeline);
if(newValue.intValue() == BlockTransactionHash.BLOCKS_TO_CONFIRM) {
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
Timeline arcRadiusTimeline = new Timeline();
KeyValue arcRadiusXValue = new KeyValue(arc.radiusXProperty(), 0.0);
KeyValue arcRadiusYValue = new KeyValue(arc.radiusYProperty(), 0.0);
@@ -33,9 +33,14 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
} else {
if(entry instanceof UtxoEntry) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
} else {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
}
Tooltip tooltip = new Tooltip();
int height = utxoEntry.getHashIndex().getHeight();
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
@@ -13,12 +13,15 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final KeyDerivation keyDerivation;
public DeviceAddressDialog(List<Device> devices, Wallet wallet, KeyDerivation keyDerivation) {
super(devices);
public DeviceAddressDialog(List<String> operationFingerprints, Wallet wallet, KeyDerivation keyDerivation) {
super(operationFingerprints);
this.wallet = wallet;
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
@@ -28,8 +31,6 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
@Subscribe
public void addressDisplayed(AddressDisplayedEvent event) {
EventManager.get().unregister(this);
setResult(event.getAddress());
this.close();
}
}
@@ -15,10 +15,12 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class DeviceDialog<R> extends Dialog<R> {
private final List<Device> operationDevices;
private final List<String> operationFingerprints;
private final Accordion deviceAccordion;
private final VBox scanBox;
private final Label scanLabel;
@@ -27,11 +29,12 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
this(null);
}
public DeviceDialog(List<Device> operationDevices) {
this.operationDevices = operationDevices;
public DeviceDialog(List<String> operationFingerprints) {
this.operationFingerprints = operationFingerprints;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -103,16 +106,20 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
}
protected void setDevices(List<Device> devices) {
List<Device> dialogDevices = devices;
if(operationDevices != null && dialogDevices.containsAll(operationDevices)) {
dialogDevices = operationDevices;
List<Device> dialogDevices = new ArrayList<>(devices);
dialogDevices.removeIf(Objects::isNull);
if(operationFingerprints != null) {
dialogDevices.removeIf(device -> {
return device.getFingerprint() != null && !operationFingerprints.contains(device.getFingerprint()) && !(device.getNeedsPinSent() || device.getNeedsPassphraseSent());
});
}
deviceAccordion.getPanes().clear();
if(dialogDevices.isEmpty()) {
scanBox.setVisible(true);
scanLabel.setText("No devices found");
scanLabel.setText("No matching devices found");
} else {
scanBox.setVisible(false);
for(Device device : dialogDevices) {
@@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -273,6 +274,8 @@ public class DevicePane extends TitledDescriptionPane {
contentBox.getChildren().add(sendPassphraseButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
Platform.runLater(passphraseField::requestFocus);
return contentBox;
}
@@ -342,8 +345,7 @@ public class DevicePane extends TitledDescriptionPane {
if(device.getFingerprint() != null) {
setPassphraseButton.setVisible(false);
device.setNeedsPassphraseSent(false);
setDefaultStatus();
setDescription("Passphrase sent");
showOperationButton();
} else {
setError("Passphrase send failed", null);
@@ -11,10 +11,13 @@ import java.util.List;
public class DeviceSignDialog extends DeviceDialog<PSBT> {
private final PSBT psbt;
public DeviceSignDialog(List<Device> devices, PSBT psbt) {
super(devices);
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
super(operationFingerprints);
this.psbt = psbt;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt);
}
@@ -26,9 +29,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
@Subscribe
public void psbtSigned(PSBTSignedEvent event) {
if(psbt == event.getPsbt()) {
EventManager.get().unregister(this);
setResult(event.getSignedPsbt());
this.close();
}
}
}
@@ -43,9 +43,18 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
} else {
if(entry instanceof TransactionEntry) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
setText("Unconfirmed Parent");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction()));
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
setText("Unconfirmed");
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction()));
} else {
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
setText(date);
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
}
Tooltip tooltip = new Tooltip();
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
setTooltip(tooltip);
@@ -95,7 +104,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
});
actionBox.getChildren().add(viewTransactionButton);
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) {
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable()) {
Button spendUtxoButton = new Button("");
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.setFontSize(12);
@@ -105,7 +114,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent())
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.collect(Collectors.toList());
if(!utxoEntries.contains(hashIndexEntry)) {
@@ -124,6 +133,20 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
}
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
public UnconfirmedTransactionContextMenu(BlockTransaction blockTransaction) {
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyTxid);
}
}
private static class TransactionContextMenu extends ContextMenu {
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
MenuItem copyDate = new MenuItem("Copy Date");
@@ -202,9 +225,12 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
public static void applyRowStyles(TreeTableCell<?, ?> cell, Entry entry) {
cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming");
cell.getStyleClass().remove("spent");
cell.getStyleClass().remove("unspendable");
if(entry != null) {
if(entry instanceof TransactionEntry) {
@@ -222,6 +248,10 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
cell.getStyleClass().add("node-row");
} else if(entry instanceof UtxoEntry) {
cell.getStyleClass().add("utxo-row");
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
} else if(entry instanceof HashIndexEntry) {
cell.getStyleClass().add("hashindex-row");
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
@@ -11,10 +11,12 @@ import javafx.scene.input.ClipboardContent;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Currency;
import java.util.Locale;
public class FiatLabel extends CopyableLabel {
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00");
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty valueProperty = new SimpleLongProperty(-1);
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
@@ -28,7 +28,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
private final FileImport importer;
private Button importButton;
protected Button importButton;
private final SimpleStringProperty password = new SimpleStringProperty("");
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl) {
@@ -44,6 +44,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
File file = fileChooser.showSaveDialog(window);
if(file != null) {
@@ -0,0 +1,106 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.io.ByteStreams;
import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
private final KeystoreFileImport importer;
private String fileName;
private byte[] fileBytes;
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
super(importer, importer.getName(), "Wallet file import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.importer = importer;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
this.fileName = fileName;
try {
fileBytes = ByteStreams.toByteArray(inputStream);
} catch(IOException e) {
throw new ImportException("Could not read file", e);
}
setContent(getScriptTypeEntry());
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, "");
Wallet wallet = new Wallet();
wallet.setName(fileName);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
private Node getScriptTypeEntry() {
Label label = new Label("Script Type:");
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
Button importFileButton = new Button("Import");
importFileButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(scriptTypeComboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
if(e.getCause() instanceof JsonParseException) {
errorMessage = "File was not in JSON format";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
importButton.setDisable(false);
}
});
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, scriptTypeComboBox, region, importFileButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
return contentBox;
}
}
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
import javafx.scene.text.Font;
import com.sparrowwallet.sparrow.AppController;
public class IdLabel extends CopyableLabel {
public IdLabel() {
@@ -9,6 +9,6 @@ public class IdLabel extends CopyableLabel {
public IdLabel(String text) {
super(text);
setFont(Font.font("Courier"));
setFont(AppController.getMonospaceFont());
}
}
@@ -26,6 +26,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -21,6 +22,7 @@ import javafx.util.Callback;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@@ -29,6 +31,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
@@ -39,7 +42,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private SplitMenuButton importButton;
private TilePane wordsPane;
private Button verifyButton;
private Button generateButton;
private Label validLabel;
private Label invalidLabel;
private Button calculateButton;
private Button backButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
@@ -47,7 +54,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
super(importer.getName(), "Mnemonic import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.importer = importer;
@@ -56,7 +63,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED + ".png");
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
@@ -74,7 +81,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Enter Mnemonic");
enterMnemonicButton.setText("Set Words Size");
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(24);
});
@@ -148,49 +155,102 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
passphraseEntry.setPadding(new Insets(0, 32, 0, 10));
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 32, 0, 10));
buttonPane.setPadding(new Insets(0, 26, 0, 10));
Button generateButton = new Button("Generate New");
generateButton = new Button("Generate New");
generateButton.setOnAction(event -> {
generateNew();
});
generateButton.managedProperty().bind(generateButton.visibleProperty());
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
buttonPane.getChildren().add(generateButton);
AnchorPane.setLeftAnchor(generateButton, 0.0);
confirmButton = new Button("Confirm Backup");
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
buttonPane.getChildren().add(confirmButton);
AnchorPane.setRightAnchor(confirmButton, 0.0);
verifyButton = new Button("Verify");
verifyButton.setDisable(true);
verifyButton.setDefaultButton(true);
verifyButton.setOnAction(event -> {
calculateButton = new Button("Calculate Seed");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
verifyButton.managedProperty().bind(verifyButton.visibleProperty());
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Calculate the seed from the provided word list"));
backButton = new Button("Back");
backButton.setOnAction(event -> {
displayMnemonicCode();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
boolean validChecksum = false;
for(String word : wordEntryList) {
if(!word.isEmpty()) {
empty = false;
}
if(!WordEntry.isValid(word)) {
verifyButton.setDisable(true);
return;
validWords = false;
}
}
verifyButton.setDisable(false);
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
}
}
generateButton.setVisible(empty && generatedMnemonicCode == null);
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
});
buttonPane.getChildren().add(verifyButton);
AnchorPane.setRightAnchor(verifyButton, 0.0);
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(backButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
}
@@ -199,14 +259,23 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
private void generateNew() {
setDescription("Write down word list to confirm backup");
showHideLink.setVisible(false);
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
displayMnemonicCode();
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
confirmButton.setVisible(true);
backButton.setVisible(false);
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
throw new IllegalStateException("Generated mnemonic words list not same size as displayed words list");
}
@@ -216,9 +285,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
wordEntry.getEditor().setEditable(false);
}
verifyButton.setVisible(false);
confirmButton.setVisible(true);
}
private void confirmBackup() {
@@ -226,6 +292,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
}
private void prepareImport() {
@@ -235,7 +303,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
setExpanded(false);
setExpanded(true);
enterMnemonicButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
@@ -277,13 +345,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(20);
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField.setMaxWidth(100);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(wordField, Validator.combine(
@@ -320,6 +389,10 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.equals(request.getUserText())) {
return Collections.emptyList();
}
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
@@ -341,7 +414,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
getChildren().addAll(passphraseLabel, passphraseField);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
}
@@ -392,4 +469,18 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntry.getEditor().setEditable(false);
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("valid-checksum");
validGlyph.setFontSize(12);
return validGlyph;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("invalid-checksum");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}
@@ -5,7 +5,7 @@ import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.UREncoder;
@@ -48,6 +48,7 @@ public class QRDisplayDialog extends Dialog<UR> {
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
qrImageView = new ImageView();
@@ -2,9 +2,12 @@ package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamResolution;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.ur.ResultType;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.URDecoder;
@@ -32,6 +35,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
WebcamView webcamView = new WebcamView(webcamService);
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
@@ -71,7 +75,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(decoder.getResult() != null) {
URDecoder.Result urResult = decoder.getResult();
if(urResult.type == ResultType.SUCCESS) {
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) {
//TODO: Confirm once UR type registry is updated
if(urResult.ur.getType().contains(UR.BYTES_TYPE) || urResult.ur.getType().equals(UR.CRYPTO_PSBT_TYPE)) {
try {
PSBT psbt = new PSBT(urResult.ur.toBytes());
result = new Result(psbt);
@@ -99,6 +104,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
} else {
PSBT psbt;
Transaction transaction;
BitcoinURI bitcoinURI;
Address address;
try {
bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI);
return;
} catch(Exception e) {
//Ignore, not an BIP 21 URI
}
try {
address = Address.fromString(qrtext);
result = new Result(address);
return;
} catch(Exception e) {
//Ignore, not an address
}
try {
psbt = PSBT.fromString(qrtext);
result = new Result(psbt);
@@ -132,9 +155,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
//Try Base43 used by Electrum
byte[] base43 = Base43.decode(qrResult.getText());
try {
psbt = new PSBT(base43);
psbt = new PSBT(Base43.decode(qrResult.getText()));
result = new Result(psbt);
return;
} catch(Exception e) {
@@ -142,14 +164,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
try {
transaction = new Transaction(base43);
transaction = new Transaction(Base43.decode(qrResult.getText()));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
result = new Result("Cannot parse QR code into a PSBT or transaction");
result = new Result("Cannot parse QR code into a PSBT, transaction or address");
}
}
}
@@ -157,12 +179,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
public final BitcoinURI uri;
public final String error;
public final Throwable exception;
public Result(Transaction transaction) {
this.transaction = transaction;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = null;
}
@@ -170,6 +194,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(PSBT psbt) {
this.transaction = null;
this.psbt = psbt;
this.uri = null;
this.error = null;
this.exception = null;
}
public Result(BitcoinURI uri) {
this.transaction = null;
this.psbt = null;
this.uri = uri;
this.error = null;
this.exception = null;
}
public Result(Address address) {
this.transaction = null;
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.error = null;
this.exception = null;
}
@@ -177,6 +218,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(String error) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = error;
this.exception = null;
}
@@ -184,6 +226,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(Throwable exception) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = exception;
}
@@ -11,6 +11,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
public SeedDisplayDialog(Keystore decryptedKeystore) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
int height = lines * 40;
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreSignedEvent;
@@ -7,8 +8,8 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
@@ -18,6 +19,9 @@ import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.control.SegmentedBar;
import java.util.ArrayList;
import java.util.List;
public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.SignatureProgressSegment> {
public SignaturesProgressBar() {
setOrientation(Orientation.HORIZONTAL);
@@ -25,10 +29,11 @@ public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.Si
setInfoNodeFactory(segment -> segment.getKeystore() == null ? null : new SignatureProgressSegmentLabel(segment.getKeystore().getLabel()));
}
public void initialize(ObservableList<Keystore> signedKeystores, int threshold) {
public void initialize(ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap, int threshold) {
getStyleClass().add("signatures-progress-bar");
getSegments().clear();
List<Keystore> signedKeystores = new ArrayList<>(signatureKeystoreMap.values());
int numSegments = Math.max(threshold, signedKeystores.size());
double segmentSize = 100d / numSegments;
for(int i = 0; i < numSegments; i++) {
@@ -39,19 +44,20 @@ public class SignaturesProgressBar extends SegmentedBar<SignaturesProgressBar.Si
}
}
signedKeystores.addListener((ListChangeListener<Keystore>) c -> {
int numSegments1 = Math.max(threshold, c.getList().size());
double newSegmentSize = 100d / numSegments1;
signatureKeystoreMap.addListener((MapChangeListener<TransactionSignature, Keystore>) c -> {
List<Keystore> newSignedKeystores = new ArrayList<>(c.getMap().values());
int newNumSegments = Math.max(threshold, newSignedKeystores.size());
double newSegmentSize = 100d / newNumSegments;
for(int i = 0; i < numSegments1; i++) {
for(int i = 0; i < newNumSegments; i++) {
SignatureProgressSegment segment = null;
if(i < getSegments().size()) {
segment = getSegments().get(i);
}
Keystore signedKeystore = null;
if(i < signedKeystores.size()) {
signedKeystore = signedKeystores.get(i);
if(i < newSignedKeystores.size()) {
signedKeystore = newSignedKeystores.get(i);
}
if(segment != null) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppController;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.scene.control.*;
@@ -24,7 +25,8 @@ public class TextAreaDialog extends Dialog<String> {
this.defaultValue = defaultValue;
getDialogPane().setContent(hbox);
dialogPane.setContent(hbox);
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
@@ -88,7 +88,7 @@ public class TitledDescriptionPane extends TitledPane {
this.layoutBoundsProperty().addListener((observable, oldValue, newValue) -> {
//Hack to force listItem to expand to full available width less border
listItem.setPrefWidth(newValue.getWidth() - 2);
listItem.setPrefWidth(newValue.getWidth() - 3);
});
return listItem;
@@ -125,7 +125,7 @@ public class TitledDescriptionPane extends TitledPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
double numLines = Math.max(1, width / 400);
double numLines = Math.max(1, Math.ceil(width / 400d));
//Handle long words like txids
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
@@ -133,7 +133,7 @@ public class TitledDescriptionPane extends TitledPane {
numLines += 1.0;
}
double height = Math.max(60, numLines * 40);
double height = Math.max(60, numLines * 20);
contentBox.setPrefHeight(height);
return contentBox;
@@ -270,10 +270,11 @@ public class TransactionDiagram extends GridPane {
outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer());
boolean isConsolidation = walletTx.isConsolidationSend();
String recipientDesc = walletTx.getRecipientAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, getSendGlyph());
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
Tooltip recipientTooltip = new Tooltip("Send " + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
outputsBox.getChildren().add(createSpacer());
@@ -323,14 +324,21 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
private Glyph getSendGlyph() {
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
sendGlyph.getStyleClass().add("send-icon");
sendGlyph.setFontSize(12);
return sendGlyph;
public static Glyph getPaymentGlyph() {
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
paymentGlyph.getStyleClass().add("payment-icon");
paymentGlyph.setFontSize(12);
return paymentGlyph;
}
private Glyph getChangeGlyph() {
public static Glyph getConsolidationGlyph() {
Glyph consolidationGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.REPLY_ALL);
consolidationGlyph.getStyleClass().add("consolidation-icon");
consolidationGlyph.setFontSize(12);
return consolidationGlyph;
}
public static Glyph getChangeGlyph() {
Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS);
changeGlyph.getStyleClass().add("change-icon");
changeGlyph.setFontSize(12);
@@ -23,8 +23,9 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
public TransactionIdDialog() {
this.txid = (CustomTextField) TextFields.createClearableTextField();
txid.setFont(Font.font ("Courier", txid.getFont().getSize()));
txid.setFont(AppController.getMonospaceFont());
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Load Transaction");
dialogPane.setHeaderText("Enter the transaction ID:");
@@ -62,7 +62,7 @@ public class UtxosChart extends BarChart<String, Number> {
}
if(utxoSeries.getData().size() > utxoDataList.size()) {
utxoSeries.getData().remove(utxoDataList.size() - 1, utxoSeries.getData().size());
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
}
if(selectedEntries != null) {
@@ -3,13 +3,12 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.ColdcardMultisig;
import com.sparrowwallet.sparrow.io.Electrum;
import com.sparrowwallet.sparrow.io.WalletExport;
import com.sparrowwallet.sparrow.io.WalletImport;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
@@ -21,8 +20,12 @@ public class WalletExportDialog extends Dialog<Wallet> {
public WalletExportDialog(Wallet wallet) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -64,9 +67,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
@Subscribe
public void walletExported(WalletExportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet();
setResult(wallet);
this.close();
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.*;
@@ -16,8 +17,12 @@ public class WalletImportDialog extends Dialog<Wallet> {
public WalletImportDialog() {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
@@ -26,16 +31,22 @@ public class WalletImportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(280);
scrollPane.setPrefHeight(320);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletImport> importers = List.of(new ColdcardMultisig(), new Electrum());
Accordion importAccordion = new Accordion();
for(WalletImport importer : importers) {
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new Electrum());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
}
@@ -44,16 +55,14 @@ public class WalletImportDialog extends Dialog<Wallet> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
dialogPane.setPrefHeight(400);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@Subscribe
public void walletImported(WalletImportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet();
setResult(wallet);
this.close();
}
}
@@ -22,6 +22,7 @@ public class WalletNameDialog extends Dialog<String> {
public WalletNameDialog() {
this.name = (CustomTextField)TextFields.createClearableTextField();
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Name");
dialogPane.setHeaderText("Enter a name for this wallet:");
@@ -36,6 +36,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText(walletName != null ? requirement.description.substring(0, requirement.description.length() - 1) + " for " + walletName + ":" : requirement.description);
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(260);
@@ -4,8 +4,12 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebcamView {
private static final Logger log = LoggerFactory.getLogger(WebcamView.class);
private final ImageView imageView;
private final WebcamService service;
private final Region view;
@@ -47,7 +51,7 @@ public class WebcamView {
imageView.imageProperty().unbind();
statusPlaceholder.setText("Error");
getChildren().setAll(statusPlaceholder);
service.getException().printStackTrace();
log.error("Failed to start web cam", service.getException());
break;
case SUCCEEDED:
// unreachable...
@@ -15,8 +15,8 @@ import org.controlsfx.control.ToggleSwitch;
public class WelcomeDialog extends Dialog<Mode> {
private static final String[] ELECTRUM_SERVERS = new String[]{
"ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx",
"electrs", "https://github.com/romanz/electrs",
"esplora-electrs", "https://github.com/Blockstream/electrs"};
"electrs (Experimental)", "https://github.com/romanz/electrs",
"esplora-electrs (Experimental)", "https://github.com/Blockstream/electrs"};
private final HostServices hostServices;
@@ -28,8 +28,9 @@ public class WelcomeDialog extends Dialog<Mode> {
setTitle("Welcome to Sparrow");
dialogPane.setHeaderText("Welcome to Sparrow!");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(450);
dialogPane.setPrefHeight(480);
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
@@ -70,7 +71,8 @@ public class WelcomeDialog extends Dialog<Mode> {
}
private HyperlinkLabel createBulletedLink(String name, String url) {
HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + name + "]");
String[] nameParts = name.split(" ");
HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + nameParts[0] + "] " + (nameParts.length > 1 ? nameParts[1] : ""));
label.setOnAction(event -> {
hostServices.showDocument(url);
});
@@ -88,6 +90,8 @@ public class WelcomeDialog extends Dialog<Mode> {
private ToggleSwitch createToggle(StatusBar statusBar, ButtonType onlineButtonType, ButtonType offlineButtonType) {
ToggleSwitch toggleSwitch = new UnlabeledToggleSwitch();
toggleSwitch.setStyle("-fx-padding: 1px 0 0 0");
toggleSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> {
Button onlineButton = (Button) getDialogPane().lookupButton(onlineButtonType);
onlineButton.setDefaultButton(newValue);
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestConnectEvent {
//Empty event class used to request programmatic connection to the server
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestDisconnectEvent {
//Empty event class used to request programmatic disconnection from the server
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.TransactionTabData;
import java.util.List;
public class TransactionTabsClosedEvent {
private final List<TransactionTabData> closedTransactionTabData;
public TransactionTabsClosedEvent(List<TransactionTabData> closedTransactionTabData) {
this.closedTransactionTabData = closedTransactionTabData;
}
public List<TransactionTabData> getClosedTransactionTabData() {
return closedTransactionTabData;
}
}
@@ -40,4 +40,8 @@ public class WalletNodeHistoryChangedEvent {
return null;
}
public String getScriptHash() {
return scriptHash;
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.WalletTabData;
import java.util.List;
public class WalletTabsClosedEvent {
private final List<WalletTabData> closedWalletTabData;
public WalletTabsClosedEvent(List<WalletTabData> closedWalletTabData) {
this.closedWalletTabData = closedWalletTabData;
}
public List<WalletTabData> getClosedWalletTabData() {
return closedWalletTabData;
}
}
@@ -15,6 +15,7 @@ public class FontAwesome5 extends GlyphFont {
* The individual glyphs offered by the FontAwesome5 font.
*/
public static enum Glyph implements INamedCharacter {
ANGLE_DOUBLE_RIGHT('\uf101'),
ARROW_DOWN('\uf063'),
ARROW_UP('\uf062'),
BTC('\uf15a'),
@@ -23,6 +24,8 @@ public class FontAwesome5 extends GlyphFont {
CIRCLE('\uf111'),
COINS('\uf51e'),
EXCLAMATION_CIRCLE('\uf06a'),
EXCLAMATION_TRIANGLE('\uf071'),
EXTERNAL_LINK_ALT('\uf35d'),
ELLIPSIS_H('\uf141'),
EYE('\uf06e'),
HAND_HOLDING('\uf4bd'),
@@ -33,10 +36,12 @@ public class FontAwesome5 extends GlyphFont {
PEN_FANCY('\uf5ac'),
QRCODE('\uf029'),
QUESTION_CIRCLE('\uf059'),
REPLY_ALL('\uf122'),
SATELLITE_DISH('\uf7c0'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
TOOLS('\uf7d9'),
UNDO('\uf0e2'),
WALLET('\uf555');
private final char ch;
@@ -21,7 +21,7 @@ public class Bip39 implements KeystoreMnemonicImport {
@Override
public String getKeystoreImportDescription() {
return "Import your 12 to 24 word mnemonic and optional passphrase";
return "Import or generate your 12 to 24 word mnemonic and optional passphrase.";
}
@Override
@@ -68,7 +68,12 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard";
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard.";
}
@Override
public String getExportFileExtension() {
return "txt";
}
@Override
@@ -138,7 +143,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getWalletImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard";
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard.";
}
@Override
@@ -191,7 +196,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getWalletExportDescription() {
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature";
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature.";
}
@Override
@@ -24,7 +24,7 @@ public class ColdcardSinglesig implements KeystoreFileImport {
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard";
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
}
@Override
@@ -14,10 +14,7 @@ import com.sparrowwallet.drongo.wallet.*;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
@@ -33,7 +30,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getKeystoreImportDescription() {
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet)";
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet).";
}
@Override
@@ -46,7 +43,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
if(!wallet.getScriptType().equals(scriptType)) {
//TODO: Derive appropriate ScriptType keystore from xprv if present
throw new ImportException("Wallet has an incompatible script type of " + wallet.getScriptType() + ", and the correct script type cannot be derived without the master private key");
throw new ImportException("Wallet has an incompatible script type of " + wallet.getScriptType() + ", and the correct script type cannot be derived without the master private key.");
}
return wallet.getKeystores().get(0);
@@ -105,11 +102,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(WalletModel.fromType(ek.hw_type));
if(keystore.getWalletModel() == null) {
throw new ImportException("Wallet has keystore of unknown hardware wallet type \"" + ek.hw_type + "\"");
throw new ImportException("Wallet has keystore of unknown hardware wallet type \"" + ek.hw_type + "\".");
}
} else if("bip32".equals(ek.type)) {
if(ek.xprv != null && ek.seed == null) {
throw new ImportException("Electrum does not support exporting BIP39 derived seeds.");
throw new ImportException("Electrum does not support exporting BIP39 derived seeds, as it does not store the mnemonic words. Only seeds created with its native Electrum Seed Version System are exportable. " +
"If you have the mnemonic words, create a new wallet with a BIP39 keystore.");
} else if(ek.seed != null) {
keystore.setSource(KeystoreSource.SW_SEED);
String mnemonic = ek.seed;
@@ -181,7 +179,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getWalletImportDescription() {
return "Import an Electrum wallet";
return "Import an Electrum wallet.";
}
@Override
public String getExportFileExtension() {
return "json";
}
@Override
@@ -263,12 +266,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public boolean isEncrypted(File file) {
return FileType.BINARY.equals(IOUtils.getFileType(file));
return (FileType.BINARY.equals(IOUtils.getFileType(file)));
}
@Override
public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file";
return "Export this wallet as an Electrum wallet file.";
}
private static class ElectrumJsonWallet {
@@ -36,7 +36,7 @@ public class Hwi {
public List<Device> enumerate(String passphrase) throws ImportException {
try {
List<String> command;
if(passphrase != null) {
if(passphrase != null && !passphrase.isEmpty()) {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
} else {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
@@ -44,7 +44,7 @@ public class Hwi {
String output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
return Arrays.asList(devices);
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
} catch(IOException e) {
throw new ImportException(e);
}
@@ -63,6 +63,7 @@ public class Hwi {
public boolean sendPin(Device device, String pin) throws ImportException {
try {
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
isPromptActive = false;
return wasSuccessful(output);
} catch(IOException e) {
throw new ImportException(e);
@@ -72,12 +73,13 @@ public class Hwi {
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
} else {
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
}
isPromptActive = false;
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
@@ -102,8 +104,9 @@ public class Hwi {
type = "--wpkh";
}
isPromptActive = false;
String output;
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
} else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
@@ -124,12 +127,14 @@ public class Hwi {
try {
String psbtBase64 = psbt.toBase64String();
isPromptActive = true;
String output;
if(passphrase != null && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_TX, psbtBase64));
} else {
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
}
isPromptActive = false;
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("psbt") != null) {
@@ -155,7 +160,6 @@ public class Hwi {
}
private String execute(List<String> command) throws IOException {
isPromptActive = false;
ProcessBuilder processBuilder = new ProcessBuilder(command);
Process process = processBuilder.start();
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
@@ -182,8 +186,8 @@ public class Hwi {
//To avoid doing these with every invocation, use a --onedir packaging and expand into a temp folder on OSX
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform.getPlatformId().toLowerCase().equals("mac")) {
InputStream inputStream = Hwi.class.getResourceAsStream("/external/" + platform.getPlatformId().toLowerCase() + "/hwi-1.1.2-mac-amd64-signed.zip");
if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/external/mac/hwi-1.1.2-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit();
@@ -211,12 +215,20 @@ public class Hwi {
hwiExecutable = tempExec;
} else {
InputStream inputStream = Hwi.class.getResourceAsStream("/external/" + platform.getPlatformId().toLowerCase() + "/hwi");
Path tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
InputStream inputStream;
Path tempExecPath;
if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/external/windows/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
} else {
inputStream = Hwi.class.getResourceAsStream("/external/linux/hwi");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
File tempExec = tempExecPath.toFile();
//tempExec.deleteOnExit();
tempExec.deleteOnExit();
OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec));
ByteStreams.copy(new GZIPInputStream(inputStream), tempExecStream);
ByteStreams.copy(inputStream, tempExecStream);
inputStream.close();
tempExecStream.flush();
tempExecStream.close();
@@ -7,7 +7,7 @@ public class IOUtils {
public static FileType getFileType(File file) {
try {
String type = Files.probeContentType(file.toPath());
if (type == null) {
if(type == null) {
if(file.getName().toLowerCase().endsWith("txn") || file.getName().toLowerCase().endsWith("psbt")) {
return FileType.TEXT;
}
@@ -17,6 +17,8 @@ public class IOUtils {
String line = br.readLine();
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
return FileType.TEXT;
} else if(line.startsWith("{")) {
return FileType.JSON;
}
}
}
@@ -14,6 +14,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.controlsfx.tools.Platform;
import java.io.*;
import java.lang.reflect.Type;
@@ -33,6 +34,7 @@ public class Storage {
private static final DateFormat BACKUP_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
public static final String SPARROW_DIR = ".sparrow";
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
public static final String WALLETS_DIR = "wallets";
public static final String WALLETS_BACKUP_DIR = "backup";
public static final String HEADER_MAGIC_1 = "SPRW1";
@@ -269,10 +271,18 @@ public class Storage {
}
static File getSparrowDir() {
if(Platform.getCurrent() == Platform.WINDOWS) {
return new File(getHomeDir(), WINDOWS_SPARROW_DIR);
}
return new File(getHomeDir(), SPARROW_DIR);
}
static File getHomeDir() {
if(Platform.getCurrent() == Platform.WINDOWS) {
return new File(System.getenv("APPDATA"));
}
return new File(System.getProperty("user.home"));
}
@@ -7,4 +7,5 @@ import java.io.OutputStream;
public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension();
}
@@ -27,7 +27,12 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
try {
FXMLLoader ksiLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/keystoreimport.fxml"));
@@ -39,7 +44,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(600);
dialogPane.setPrefHeight(620);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
} catch(IOException e) {
@@ -55,6 +60,5 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore();
setResult(keystore);
this.close();
}
}
@@ -167,7 +167,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids) {
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, VerboseTransaction> batchRequest = client.createBatchRequest().keysType(String.class).returnType(VerboseTransaction.class);
for(String txid : txids) {
@@ -370,7 +370,7 @@ public class ElectrumServer {
}
BlockTransactionHash reference = optionalReference.get();
Date blockDate;
Date blockDate = null;
if(reference.getHeight() > 0) {
BlockHeader blockHeader = blockHeaderMap.get(reference.getHeight());
if(blockHeader == null) {
@@ -379,8 +379,6 @@ public class ElectrumServer {
continue;
}
blockDate = blockHeader.getTimeAsDate();
} else {
blockDate = new Date();
}
BlockTransaction blockchainTransaction = new BlockTransaction(reference.getHash(), reference.getHeight(), blockDate, reference.getFee(), transaction);
@@ -479,13 +477,13 @@ public class ElectrumServer {
}
}
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references) throws ServerException {
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references, String scriptHash) throws ServerException {
Set<String> txids = new LinkedHashSet<>(references.size());
for(Sha256Hash reference : references) {
txids.add(reference.toString());
}
Map<String, VerboseTransaction> result = electrumServerRpc.getVerboseTransactions(getTransport(), txids);
Map<String, VerboseTransaction> result = electrumServerRpc.getVerboseTransactions(getTransport(), txids, scriptHash);
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
for(String txid : result.keySet()) {
@@ -535,13 +533,13 @@ public class ElectrumServer {
return Utils.bytesToHex(reversed);
}
private String getScriptHash(TransactionOutput output) {
public static String getScriptHash(TransactionOutput output) {
byte[] hash = Sha256Hash.hash(output.getScript().getProgram());
byte[] reversed = Utils.reverseBytes(hash);
return Utils.bytesToHex(reversed);
}
static Map<String, String> getSubscribedScriptHashes() {
public static Map<String, String> getSubscribedScriptHashes() {
return subscribedScriptHashes;
}
@@ -579,7 +577,6 @@ public class ElectrumServer {
private final boolean subscribe;
private boolean firstCall = true;
private Thread reader;
private Throwable lastReaderException;
private long feeRatesRetrievedAt;
public ConnectionService() {
@@ -598,7 +595,7 @@ public class ElectrumServer {
if(firstCall) {
electrumServer.connect();
reader = new Thread(new ReadRunnable());
reader = new Thread(new ReadRunnable(), "ElectrumServerReadThread");
reader.setDaemon(true);
reader.setUncaughtExceptionHandler(ConnectionService.this);
reader.start();
@@ -639,8 +636,7 @@ public class ElectrumServer {
return new FeeRatesUpdatedEvent(blockTargetFeeRates);
}
} else {
firstCall = true;
throw new ServerException("Connection to server failed", lastReaderException);
resetConnection();
}
}
@@ -649,12 +645,21 @@ public class ElectrumServer {
};
}
public void resetConnection() {
try {
closeActiveConnection();
firstCall = true;
} catch (ServerException e) {
log.error("Error closing connection during connection reset", e);
}
}
@Override
public boolean cancel() {
try {
closeActiveConnection();
} catch (ServerException e) {
log.error("Eror closing connection", e);
log.error("Error closing connection", e);
}
return super.cancel();
@@ -664,12 +669,11 @@ public class ElectrumServer {
public void reset() {
super.reset();
firstCall = true;
lastReaderException = null;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
this.lastReaderException = e;
log.error("Uncaught error in ConnectionService", e);
}
}
@@ -679,8 +683,9 @@ public class ElectrumServer {
try {
TcpTransport tcpTransport = (TcpTransport)getTransport();
tcpTransport.readInputLoop();
} catch (ServerException e) {
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
} catch(ServerException e) {
//Only debug logging here as the exception has been passed on to the ConnectionService thread via TcpTransport
log.debug("Read thread terminated", e);
}
}
}
@@ -708,6 +713,7 @@ public class ElectrumServer {
public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> {
private final Set<Sha256Hash> references;
private String scriptHash;
public TransactionReferenceService(Transaction transaction) {
references = new HashSet<>();
@@ -717,6 +723,11 @@ public class ElectrumServer {
}
}
public TransactionReferenceService(Set<Sha256Hash> references, String scriptHash) {
this(references);
this.scriptHash = scriptHash;
}
public TransactionReferenceService(Set<Sha256Hash> references) {
this.references = references;
}
@@ -726,7 +737,7 @@ public class ElectrumServer {
return new Task<>() {
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.getReferencedTransactions(references);
return electrumServer.getReferencedTransactions(references, scriptHash);
}
};
}
@@ -25,7 +25,7 @@ public interface ElectrumServerRpc {
Map<String, String> getTransactions(Transport transport, Set<String> txids);
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids);
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash);
Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks);
@@ -1,22 +1,22 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import static com.sparrowwallet.drongo.protocol.Transaction.DUST_RELAY_TX_FEE;
public class SimpleElectrumServerRpc implements ElectrumServerRpc {
private static final Logger log = LoggerFactory.getLogger(SimpleElectrumServerRpc.class);
private static final int MAX_TARGET_BLOCKS = 25;
@Override
public void ping(Transport transport) {
@@ -156,7 +156,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
}
@Override
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids) {
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash) {
JsonRpcClient client = new JsonRpcClient(transport);
Map<String, VerboseTransaction> result = new LinkedHashMap<>();
@@ -164,10 +164,37 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
try {
VerboseTransaction verboseTransaction = client.createRequest().returnAs(VerboseTransaction.class).method("blockchain.transaction.get").id(txid).params(txid, true).execute();
result.put(txid, verboseTransaction);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Error retrieving transaction: " + txid + " (" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()) + ")");
} catch(JsonRpcException e) {
log.warn("Error retrieving transaction: " + txid + " (" + e.getErrorMessage() + ")");
} catch(Exception e) {
//electrs does not currently support the verbose parameter, so try to fetch an incomplete VerboseTransaction without it
//Note that without the script hash associated with the transaction, we can't get a block height as there is no way in the Electrum RPC protocol to do this
//We mark this VerboseTransaction as incomplete by assigning it a Sha256Hash.ZERO_HASH blockhash
log.debug("Error retrieving transaction: " + txid + " (" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()) + ")");
try {
String rawTxHex = client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(txid).params(txid).execute();
Transaction tx = new Transaction(Utils.hexToBytes(rawTxHex));
String id = tx.getTxId().toString();
int height = 0;
if(scriptHash != null) {
ScriptHashTx[] scriptHashTxes = client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(id).params(scriptHash).execute();
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
if(scriptHashTx.tx_hash.equals(id)) {
height = scriptHashTx.height;
break;
}
}
}
VerboseTransaction verboseTransaction = new VerboseTransaction();
verboseTransaction.txid = id;
verboseTransaction.hex = rawTxHex;
verboseTransaction.confirmations = (height <= 0 ? 0 : AppController.getCurrentBlockHeight() - height + 1);
verboseTransaction.blockhash = Sha256Hash.ZERO_HASH.toString();
result.put(txid, verboseTransaction);
} catch(Exception ex) {
throw new ElectrumServerRpcException("Error retrieving transaction: ", ex);
}
}
}
@@ -180,14 +207,18 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<Integer, Double> result = new LinkedHashMap<>();
for(Integer targetBlock : targetBlocks) {
try {
Double targetBlocksFeeRateBtcKb = client.createRequest().returnAs(Double.class).method("blockchain.estimatefee").id(targetBlock).params(targetBlock).execute();
result.put(targetBlock, targetBlocksFeeRateBtcKb);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Failed to retrieve fee rate for target blocks: " + targetBlock + " (" + e.getMessage() + ")");
result.put(targetBlock, DUST_RELAY_TX_FEE);
} catch(JsonRpcException e) {
throw new ElectrumServerRpcException("Failed to retrieve fee rate for target blocks: " + targetBlock, e);
if(targetBlock <= MAX_TARGET_BLOCKS) {
try {
Double targetBlocksFeeRateBtcKb = client.createRequest().returnAs(Double.class).method("blockchain.estimatefee").id(targetBlock).params(targetBlock).execute();
result.put(targetBlock, targetBlocksFeeRateBtcKb);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Failed to retrieve fee rate for target blocks: " + targetBlock + " (" + e.getMessage() + ")");
result.put(targetBlock, result.values().stream().mapToDouble(v -> v).min().orElse(0.0001d));
} catch(JsonRpcException e) {
throw new ElectrumServerRpcException("Failed to retrieve fee rate for target blocks: " + targetBlock, e);
}
} else {
result.put(targetBlock, result.values().stream().mapToDouble(v -> v).min().orElse(0.0001d));
}
}
@@ -5,6 +5,8 @@ import com.github.arteam.simplejsonrpc.server.JsonRpcServer;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.io.Config;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.SocketFactory;
import java.io.*;
@@ -12,6 +14,8 @@ import java.net.Socket;
import java.util.concurrent.locks.ReentrantLock;
public class TcpTransport implements Transport, Closeable {
private static final Logger log = LoggerFactory.getLogger(TcpTransport.class);
public static final int DEFAULT_PORT = 50001;
protected final HostAndPort server;
@@ -24,10 +28,13 @@ public class TcpTransport implements Transport, Closeable {
private final ReentrantLock clientRequestLock = new ReentrantLock();
private boolean running = false;
private boolean reading = true;
private boolean firstRead = true;
private final JsonRpcServer jsonRpcServer = new JsonRpcServer();
private final SubscriptionService subscriptionService = new SubscriptionService();
private Exception lastException;
public TcpTransport(HostAndPort server) {
this.server = server;
this.socketFactory = SocketFactory.getDefault();
@@ -50,7 +57,12 @@ public class TcpTransport implements Transport, Closeable {
out.flush();
}
private synchronized String readResponse() {
private synchronized String readResponse() throws IOException {
if(firstRead) {
notifyAll();
firstRead = false;
}
while(reading) {
try {
wait();
@@ -60,6 +72,10 @@ public class TcpTransport implements Transport, Closeable {
}
}
if(lastException != null) {
throw new IOException("Error reading response: " + lastException.getMessage(), lastException);
}
reading = true;
notifyAll();
@@ -67,6 +83,13 @@ public class TcpTransport implements Transport, Closeable {
}
public synchronized void readInputLoop() throws ServerException {
try {
//Don't start reading until first RPC request is sent
wait();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
while(running) {
try {
String received = readInputStream();
@@ -83,9 +106,13 @@ public class TcpTransport implements Transport, Closeable {
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
} catch(IOException e) {
} catch(Exception e) {
if(running) {
throw new ServerException(e);
lastException = e;
reading = false;
notifyAll();
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
running = false;
}
}
}
@@ -22,6 +22,10 @@ class VerboseTransaction {
public int version;
public int getHeight() {
if(confirmations <= 0) {
return 0;
}
Integer currentHeight = AppController.getCurrentBlockHeight();
if(currentHeight != null) {
return currentHeight - confirmations + 1;
@@ -31,6 +35,11 @@ class VerboseTransaction {
}
public Date getDate() {
if(blocktime == 0) {
//Ok to return as null here as date inspection for verbose txes is only done by HeadersController, which checks for null values
return null;
}
return new Date(blocktime * 1000);
}
@@ -1,7 +1,10 @@
package com.sparrowwallet.sparrow.preferences;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
@@ -12,6 +15,8 @@ import org.controlsfx.tools.Borders;
import java.io.IOException;
public class PreferencesDialog extends Dialog<Boolean> {
private final boolean existingConnection;
public PreferencesDialog() {
this(null);
}
@@ -22,6 +27,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
public PreferencesDialog(PreferenceGroup initialGroup, boolean initialSetup) {
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
try {
FXMLLoader preferencesLoader = new FXMLLoader(AppController.class.getResource("preferences/preferences.fxml"));
@@ -45,6 +51,13 @@ public class PreferencesDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(500);
existingConnection = ElectrumServer.isConnected();
setOnCloseRequest(event -> {
if(existingConnection && !ElectrumServer.isConnected()) {
EventManager.get().post(new RequestConnectEvent());
}
});
setResultConverter(dialogButton -> dialogButton == newWalletButtonType ? Boolean.TRUE : null);
} catch(IOException e) {
throw new RuntimeException(e);
@@ -1,9 +1,11 @@
package com.sparrowwallet.sparrow.preferences;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.TextFieldValidator;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.ConnectionEvent;
import com.sparrowwallet.sparrow.event.RequestDisconnectEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -61,6 +63,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
@FXML
private Button testConnection;
@FXML
private Button editConnection;
@FXML
private TextArea testResults;
@@ -125,33 +130,36 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
});
testConnection.setDisable(ElectrumServer.isConnected());
boolean isConnected = ElectrumServer.isConnected();
setFieldsEditable(!isConnected);
testConnection.managedProperty().bind(testConnection.visibleProperty());
testConnection.setVisible(!isConnected);
testConnection.setOnAction(event -> {
testResults.setText("Connecting to " + config.getElectrumServer() + "...");
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
boolean existingConnection = ElectrumServer.isConnected();
if(existingConnection) {
ElectrumServer.ServerBannerService serverBannerService = new ElectrumServer.ServerBannerService();
serverBannerService.setOnSucceeded(successEvent -> {
showConnectionSuccess(null, serverBannerService.getValue());
});
serverBannerService.setOnFailed(this::showConnectionFailure);
serverBannerService.start();
} else {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1));
connectionService.setOnSucceeded(successEvent -> {
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel();
});
connectionService.setOnFailed(workerStateEvent -> {
showConnectionFailure(workerStateEvent);
connectionService.cancel();
});
connectionService.start();
}
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1));
connectionService.setOnSucceeded(successEvent -> {
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel();
});
connectionService.setOnFailed(workerStateEvent -> {
showConnectionFailure(workerStateEvent);
connectionService.cancel();
});
connectionService.start();
});
editConnection.managedProperty().bind(editConnection.visibleProperty());
editConnection.setVisible(isConnected);
editConnection.setOnAction(event -> {
EventManager.get().post(new RequestDisconnectEvent());
setFieldsEditable(true);
editConnection.setVisible(false);
testConnection.setVisible(true);
});
String electrumServer = config.getElectrumServer();
@@ -196,6 +204,17 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
private void setFieldsEditable(boolean editable) {
host.setEditable(editable);
port.setEditable(editable);
useSsl.setDisable(!editable);
certificate.setEditable(editable);
certificateSelect.setDisable(!editable);
useProxy.setDisable(!editable);
proxyHost.setEditable(editable);
proxyPort.setEditable(editable);
}
private void showConnectionSuccess(List<String> serverVersion, String serverBanner) {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, Color.rgb(80, 161, 79)));
if(serverVersion != null) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
@@ -14,6 +15,9 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -92,9 +96,18 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private Spinner<Integer> locktimeBlock;
@FXML
private Hyperlink locktimeCurrentHeight;
@FXML
private Label futureBlockWarning;
@FXML
private DateTimePicker locktimeDate;
@FXML
private Label futureDateWarning;
@FXML
private CopyableLabel size;
@@ -192,6 +205,11 @@ public class HeadersController extends TransactionFormController implements Init
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return headersForm;
}
private void initializeView() {
Transaction tx = headersForm.getTransaction();
@@ -227,6 +245,8 @@ public class HeadersController extends TransactionFormController implements Init
locktimeFieldset.getChildren().add(locktimeBlockField);
Integer block = locktimeBlock.getValue();
if(block != null) {
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppController.getCurrentBlockHeight() != null && block < AppController.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && block > AppController.getCurrentBlockHeight());
tx.setLocktime(block);
if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
@@ -240,6 +260,7 @@ public class HeadersController extends TransactionFormController implements Init
LocalDateTime date = locktimeDate.getDateTimeValue();
if(date != null) {
locktimeDate.setDateTimeValue(date);
futureDateWarning.setVisible(date.isAfter(LocalDateTime.now()));
tx.setLocktime(date.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
@@ -249,6 +270,13 @@ public class HeadersController extends TransactionFormController implements Init
}
});
locktimeCurrentHeight.managedProperty().bind(locktimeCurrentHeight.visibleProperty());
locktimeCurrentHeight.setVisible(false);
futureBlockWarning.managedProperty().bind(futureBlockWarning.visibleProperty());
futureBlockWarning.setVisible(false);
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
futureDateWarning.setVisible(false);
locktimeNone.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
@@ -268,6 +296,8 @@ public class HeadersController extends TransactionFormController implements Init
locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> {
tx.setLocktime(newValue);
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppController.getCurrentBlockHeight() != null && newValue < AppController.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && newValue > AppController.getCurrentBlockHeight());
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
EventManager.get().post(new TransactionLocktimeChangedEvent(tx));
@@ -277,16 +307,19 @@ public class HeadersController extends TransactionFormController implements Init
locktimeDate.setFormat(LOCKTIME_DATE_FORMAT);
locktimeDate.dateTimeValueProperty().addListener((obs, oldValue, newValue) -> {
tx.setLocktime(newValue.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
futureDateWarning.setVisible(newValue.isAfter(LocalDateTime.now()));
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
}
});
locktimeNoneType.setDisable(!headersForm.isEditable());
locktimeBlockType.setDisable(!headersForm.isEditable());
locktimeDateType.setDisable(!headersForm.isEditable());
locktimeBlock.setDisable(!headersForm.isEditable());
locktimeDate.setDisable(!headersForm.isEditable());
boolean locktimeEnabled = headersForm.getTransaction().isLocktimeSequenceEnabled();
locktimeNoneType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeBlockType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeDateType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeBlock.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeDate.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeCurrentHeight.setDisable(!headersForm.isEditable() || !locktimeEnabled);
updateSize();
@@ -372,7 +405,7 @@ public class HeadersController extends TransactionFormController implements Init
updateSignedKeystores(signingWallet);
int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired();
signaturesProgressBar.initialize(headersForm.getSignedKeystores(), threshold);
signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold);
});
EventManager.get().post(new RequestOpenWalletsEvent());
@@ -436,7 +469,10 @@ public class HeadersController extends TransactionFormController implements Init
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
blockchainForm.setVisible(true);
if(currentHeight == null) {
if(Sha256Hash.ZERO_HASH.equals(blockTransaction.getBlockHash()) && blockTransaction.getHeight() == 0 && headersForm.getSigningWallet() == null) {
//A zero block hash indicates that this blocktransaction is incomplete and the height is likely incorrect if we are not sending a tx
blockStatus.setText("Unknown");
} else if(currentHeight == null) {
blockStatus.setText(blockTransaction.getHeight() > 0 ? "Confirmed" : "Unconfirmed");
} else {
int confirmations = blockTransaction.getHeight() > 0 ? currentHeight - blockTransaction.getHeight() + 1 : 0;
@@ -479,7 +515,7 @@ public class HeadersController extends TransactionFormController implements Init
blockTimestampField.setVisible(false);
}
if(blockTransaction.getBlockHash() != null) {
if(blockTransaction.getBlockHash() != null && !blockTransaction.getBlockHash().equals(Sha256Hash.ZERO_HASH)) {
blockHashField.setVisible(true);
blockHash.setText(blockTransaction.getBlockHash().toString());
blockHash.setContextMenu(new BlockHeightContextMenu(blockTransaction.getBlockHash()));
@@ -530,6 +566,13 @@ public class HeadersController extends TransactionFormController implements Init
Clipboard.getSystemClipboard().setContent(content);
}
public void setLocktimeToCurrentHeight(ActionEvent event) {
if(AppController.getCurrentBlockHeight() != null && locktimeBlock.isEditable()) {
locktimeBlock.getValueFactory().setValue(AppController.getCurrentBlockHeight());
Platform.runLater(() -> locktimeBlockType.requestFocus());
}
}
public void openWallet(ActionEvent event) {
EventManager.get().post(new RequestWalletOpenEvent());
}
@@ -646,9 +689,7 @@ public class HeadersController extends TransactionFormController implements Init
return;
}
signingDevices.addAll(AppController.getDevices().stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList()));
DeviceSignDialog dlg = new DeviceSignDialog(signingDevices.isEmpty() ? null : signingDevices, headersForm.getPsbt());
DeviceSignDialog dlg = new DeviceSignDialog(fingerprints, headersForm.getPsbt());
dlg.initModality(Modality.NONE);
Stage stage = (Stage)dlg.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true);
@@ -662,12 +703,11 @@ public class HeadersController extends TransactionFormController implements Init
}
private void updateSignedKeystores(Wallet signingWallet) {
Map<PSBTInput, List<Keystore>> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt());
Optional<List<Keystore>> optSignedKeystores = signedKeystoresMap.values().stream().filter(list -> !list.isEmpty()).min(Comparator.comparingInt(List::size));
Map<PSBTInput, Map<TransactionSignature, Keystore>> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt());
Optional<Map<TransactionSignature, Keystore>> optSignedKeystores = signedKeystoresMap.values().stream().filter(map -> !map.isEmpty()).min(Comparator.comparingInt(Map::size));
optSignedKeystores.ifPresent(signedKeystores -> {
List<Keystore> newSignedKeystores = new ArrayList<>(signedKeystores);
newSignedKeystores.removeAll(headersForm.getSignedKeystores());
headersForm.getSignedKeystores().addAll(newSignedKeystores);
headersForm.getSignatureKeystoreMap().keySet().retainAll(signedKeystores.keySet());
headersForm.getSignatureKeystoreMap().putAll(signedKeystores);
});
}
@@ -693,6 +733,9 @@ public class HeadersController extends TransactionFormController implements Init
if(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet) {
//Ensure the script hashes of the UTXOs in FinalizingPSBTWallet are subscribed to
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(headersForm.getSigningWallet());
historyService.setOnFailed(workerStateEvent -> {
log.error("Error subscribing FinalizingPSBTWallet script hashes", workerStateEvent.getSource().getException());
});
historyService.start();
}
@@ -702,7 +745,8 @@ public class HeadersController extends TransactionFormController implements Init
});
broadcastTransactionService.setOnFailed(workerStateEvent -> {
broadcastProgressBar.setProgress(0);
AppController.showErrorDialog("Error broadcasting transaction", workerStateEvent.getSource().getException().getMessage());
log.error("Error broadcasting transaction", workerStateEvent.getSource().getException());
AppController.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in sparrow.log");
broadcastButton.setDisable(false);
});
@@ -745,13 +789,14 @@ public class HeadersController extends TransactionFormController implements Init
locktimeBlock.setDisable(!locktimeEnabled);
locktimeDateType.setDisable(!locktimeEnabled);
locktimeDate.setDisable(!locktimeEnabled);
locktimeCurrentHeight.setDisable(!locktimeEnabled);
}
}
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(headersForm.getTransaction().getTxId())) {
if(event.getBlockTransaction() != null) {
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || headersForm.getBlockTransaction() == null)) {
updateBlockchainForm(event.getBlockTransaction(), AppController.getCurrentBlockHeight());
}
@@ -785,6 +830,7 @@ public class HeadersController extends TransactionFormController implements Init
signaturesForm.setVisible(true);
headersForm.setSigningWallet(availableWallets.get(0));
signButton.setDisable(false);
if(headersForm.getPsbt().isSigned()) {
finalizePSBT();
@@ -801,6 +847,7 @@ public class HeadersController extends TransactionFormController implements Init
noWalletsWarning.setVisible(false);
signingWallet.setVisible(true);
finalizeTransaction.setDisable(false);
signButton.setDisable(false);
}
} else {
if(headersForm.getPsbt().isSigned()) {
@@ -817,6 +864,7 @@ public class HeadersController extends TransactionFormController implements Init
noWalletsWarning.setVisible(true);
signingWallet.setVisible(false);
finalizeTransaction.setDisable(true);
signButton.setDisable(true);
}
}
}
@@ -831,6 +879,7 @@ public class HeadersController extends TransactionFormController implements Init
locktimeDateType.setDisable(true);
locktimeBlock.setDisable(true);
locktimeDate.setDisable(true);
locktimeCurrentHeight.setVisible(false);
updateTxId();
headersForm.setSigningWallet(event.getSigningWallet());
@@ -873,6 +922,7 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe
public void keystoreSigned(KeystoreSignedEvent event) {
if(headersForm.getSignedKeystores().contains(event.getKeystore()) && headersForm.getPsbt() != null) {
//Attempt to finalize PSBT - will do nothing if all inputs are not signed
finalizePSBT();
}
}
@@ -891,7 +941,7 @@ public class HeadersController extends TransactionFormController implements Init
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(headersForm.getSigningWallet() != null && event.getWalletNode(headersForm.getSigningWallet()) != null) {
Sha256Hash txid = headersForm.getTransaction().getTxId();
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid));
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash());
transactionReferenceService.setOnSucceeded(successEvent -> {
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
BlockTransaction blockTransaction = transactionMap.get(txid);
@@ -901,14 +951,47 @@ public class HeadersController extends TransactionFormController implements Init
updateBlockchainForm(blockTransaction, AppController.getCurrentBlockHeight());
}
});
transactionReferenceService.setOnFailed(failEvent -> {
log.error("Could not update block transaction", failEvent.getSource().getException());
});
transactionReferenceService.start();
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
//Update tx and input/output reference labels on history changed wallet if this txid matches and label is null
if(headersForm.getSigningWallet() != null && !(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet)) {
Sha256Hash txid = headersForm.getTransaction().getTxId();
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid);
if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel(headersForm.getName());
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()))));
}
for(WalletNode walletNode : event.getHistoryChangedNodes()) {
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
output.setLabel(headersForm.getName() + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()))));
}
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
output.getSpentBy().setLabel(headersForm.getName() + " (input)");
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output.getSpentBy(), HashIndexEntry.Type.INPUT, walletNode.getKeyPurpose()))));
}
}
}
}
}
@Subscribe
public void newBlock(NewBlockEvent event) {
if(headersForm.getBlockTransaction() != null) {
updateBlockchainForm(headersForm.getBlockTransaction(), event.getHeight());
}
if(futureBlockWarning.isVisible()) {
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && locktimeBlock.getValue() > event.getHeight());
}
}
}
@@ -11,7 +11,7 @@ import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
@@ -42,9 +42,6 @@ public class InputController extends TransactionFormController implements Initia
@FXML
private Hyperlink linkedOutpoint;
@FXML
private Button outpointSelect;
@FXML
private CoinLabel spends;
@@ -129,7 +126,7 @@ public class InputController extends TransactionFormController implements Initia
initializeLocktimeFields(txInput);
if(psbtInput != null) {
inputForm.getSignedKeystores().addListener((ListChangeListener<Keystore>) c -> {
inputForm.getSignatureKeystoreMap().addListener((MapChangeListener<TransactionSignature, Keystore>) c -> {
updateSignatures(inputForm.getPsbtInput());
});
}
@@ -143,7 +140,6 @@ public class InputController extends TransactionFormController implements Initia
if(txInput.isCoinBase()) {
outpoint.setText("Coinbase");
outpointSelect.setVisible(false);
long totalAmt = 0;
for(TransactionOutput output : inputForm.getTransaction().getOutputs()) {
totalAmt += output.getValue();
@@ -164,9 +160,6 @@ public class InputController extends TransactionFormController implements Initia
} else if(inputForm.getInputTransactions() != null) {
updateSpends(inputForm.getInputTransactions());
}
//TODO: Enable select outpoint when wallet present
outpointSelect.setDisable(true);
}
private void updateOutpoint(Map<Sha256Hash, BlockTransaction> inputTransactions) {
@@ -461,6 +454,11 @@ public class InputController extends TransactionFormController implements Initia
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return inputForm;
}
@Override
protected String describeScriptChunk(ScriptChunk chunk) {
String chunkString = super.describeScriptChunk(chunk);
@@ -13,7 +13,7 @@ import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
import com.sparrowwallet.sparrow.event.PSBTCombinedEvent;
import com.sparrowwallet.sparrow.event.PSBTFinalizedEvent;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.PieChart;
@@ -49,6 +49,11 @@ public class InputsController extends TransactionFormController implements Initi
initialiseView();
}
@Override
protected TransactionForm getTransactionForm() {
return inputsForm;
}
private void initialiseView() {
Transaction tx = inputsForm.getTransaction();
count.setText(Integer.toString(tx.getInputs().size()));
@@ -57,7 +62,7 @@ public class InputsController extends TransactionFormController implements Initi
if(inputsForm.getPsbt() != null) {
updatePSBTInputs(inputsForm.getPsbt());
inputsForm.getSignedKeystores().addListener((ListChangeListener<Keystore>) c -> {
inputsForm.getSignatureKeystoreMap().addListener((MapChangeListener<TransactionSignature, Keystore>) c -> {
updatePSBTInputs(inputsForm.getPsbt());
});
} else if(inputsForm.getInputTransactions() != null) {
@@ -1,16 +1,15 @@
package com.sparrowwallet.sparrow.transaction;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressLabel;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.ScriptArea;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
@@ -38,9 +37,6 @@ public class OutputController extends TransactionFormController implements Initi
@FXML
private CopyableLabel to;
@FXML
private CopyableLabel change;
@FXML
private AddressLabel address;
@@ -67,7 +63,10 @@ public class OutputController extends TransactionFormController implements Initi
public void initializeView() {
TransactionOutput txOutput = outputForm.getTransactionOutput();
outputFieldset.setText("Output #" + txOutput.getIndex());
outputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
updateOutputLegendFromWallet(txOutput, signingWallet);
});
updateOutputLegendFromWallet(txOutput, outputForm.getSigningWallet());
value.setValue(txOutput.getValue());
to.setVisible(false);
@@ -83,12 +82,6 @@ public class OutputController extends TransactionFormController implements Initi
//ignore
}
change.managedProperty().bind(change.visibleProperty());
change.setVisible(false);
outputForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> {
change.setVisible(signingWallet != null && signingWallet.isWalletOutputScript(txOutput.getScript()));
});
spentField.managedProperty().bind(spentField.visibleProperty());
spentByField.managedProperty().bind(spentByField.visibleProperty());
spentByField.setVisible(false);
@@ -106,6 +99,29 @@ public class OutputController extends TransactionFormController implements Initi
scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
}
private String getLegendText(TransactionOutput txOutput) {
return "Output #" + txOutput.getIndex();
}
private void updateOutputLegendFromWallet(TransactionOutput txOutput, Wallet signingWallet) {
String baseText = getLegendText(txOutput);
if(signingWallet != null) {
if(signingWallet.getWalletOutputScripts(KeyPurpose.RECEIVE).containsKey(txOutput.getScript())) {
outputFieldset.setText(baseText + " - Consolidation");
outputFieldset.setIcon(TransactionDiagram.getConsolidationGlyph());
} else if(signingWallet.getWalletOutputScripts(KeyPurpose.CHANGE).containsKey(txOutput.getScript())) {
outputFieldset.setText(baseText + " - Change");
outputFieldset.setIcon(TransactionDiagram.getChangeGlyph());
} else {
outputFieldset.setText(baseText + " - Payment");
outputFieldset.setIcon(TransactionDiagram.getPaymentGlyph());
}
} else {
outputFieldset.setText(baseText);
outputFieldset.setIcon(null);
}
}
private void updateSpent(List<BlockTransaction> outputTransactions) {
int outputIndex = outputForm.getIndex();
if(outputIndex < outputForm.getMaxOutputFetched()) {
@@ -147,6 +163,11 @@ public class OutputController extends TransactionFormController implements Initi
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return outputForm;
}
@Subscribe
public void blockTransactionOutputsFetched(BlockTransactionOutputsFetchedEvent event) {
if(event.getTxId().equals(outputForm.getTransaction().getTxId()) && outputForm.getPsbt() == null && outputForm.getIndex() >= event.getPageStart() && outputForm.getIndex() < event.getPageEnd()) {
@@ -51,6 +51,11 @@ public class OutputsController extends TransactionFormController implements Init
}
}
@Override
protected TransactionForm getTransactionForm() {
return outputsForm;
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
total.refresh(event.getBitcoinUnit());
@@ -13,7 +13,7 @@ public class PageForm extends IndexedTransactionForm {
private final int pageEnd;
public PageForm(TransactionView view, int pageStart, int pageEnd) {
super(new TransactionData(ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION), pageStart);
super(new TransactionData(pageStart + "-" + pageEnd, ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION), pageStart);
this.view = view;
this.pageStart = pageStart;
this.pageEnd = pageEnd;
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.control.TransactionHexArea;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -318,9 +319,9 @@ public class TransactionController implements Initializable {
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
BlockTransaction thisBlockTx = null;
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
for (Sha256Hash txid : transactionMap.keySet()) {
for(Sha256Hash txid : transactionMap.keySet()) {
BlockTransaction blockTx = transactionMap.get(txid);
if (txid.equals(getTransaction().getTxId())) {
if(txid.equals(getTransaction().getTxId())) {
thisBlockTx = blockTx;
} else {
inputTransactions.put(txid, blockTx);
@@ -340,7 +341,7 @@ public class TransactionController implements Initializable {
});
});
transactionReferenceService.setOnFailed(failedEvent -> {
failedEvent.getSource().getException().printStackTrace();
log.error("Error fetching transaction or input references", failedEvent.getSource().getException());
});
transactionReferenceService.start();
}
@@ -357,40 +358,28 @@ public class TransactionController implements Initializable {
});
});
transactionOutputsReferenceService.setOnFailed(failedEvent -> {
failedEvent.getSource().getException().printStackTrace();
log.error("Error fetching transaction output references", failedEvent.getSource().getException());
});
transactionOutputsReferenceService.start();
}
}
public void setTransactionData(TransactionData transactionData) {
this.txdata = transactionData;
}
public Transaction getTransaction() {
return txdata.getTransaction();
}
public void setTransaction(Transaction transaction) {
this.txdata = new TransactionData(transaction);
}
public void setName(String name) {
this.txdata.setName(name);
}
public PSBT getPSBT() {
return txdata.getPsbt();
}
public void setPSBT(PSBT psbt) {
this.txdata = new TransactionData(psbt);
}
public BlockTransaction getBlockTransaction() {
return txdata.getBlockTransaction();
}
public void setBlockTransaction(BlockTransaction blockTransaction) {
this.txdata = new TransactionData(blockTransaction);
}
public void setInitialView(TransactionView initialView, Integer initialIndex) {
this.initialView = initialView;
this.initialIndex = initialIndex;
@@ -467,7 +456,9 @@ public class TransactionController implements Initializable {
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(getTransaction().getTxId())) {
txdata.setBlockTransaction(event.getBlockTransaction());
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || txdata.getBlockTransaction() == null)) {
txdata.setBlockTransaction(event.getBlockTransaction());
}
if(txdata.getInputTransactions() == null) {
txdata.setInputTransactions(event.getInputTransactions());
} else {
@@ -501,4 +492,13 @@ public class TransactionController implements Initializable {
highlightTxHex();
}
}
@Subscribe
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
if(tabData.getTransactionData() == txdata) {
EventManager.get().unregister(this);
}
}
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
@@ -9,9 +10,10 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -30,19 +32,20 @@ public class TransactionData {
private final ObservableMap<Wallet, Storage> availableWallets = FXCollections.observableHashMap();
private final SimpleObjectProperty<Wallet> signingWallet = new SimpleObjectProperty<>(this, "signingWallet", null);
private final ObservableList<Keystore> signedKeystores = FXCollections.observableArrayList();
private final ObservableMap<TransactionSignature, Keystore> signatureKeystoreMap = FXCollections.observableMap(new LinkedHashMap<>());
public TransactionData(PSBT psbt) {
this.transaction = psbt.getTransaction();
public TransactionData(String name, PSBT psbt) {
this(name, psbt.getTransaction());
this.psbt = psbt;
}
public TransactionData(BlockTransaction blockTransaction) {
this.transaction = blockTransaction.getTransaction();
public TransactionData(String name, BlockTransaction blockTransaction) {
this(name, blockTransaction.getTransaction());
this.blockTransaction = blockTransaction;
}
public TransactionData(Transaction transaction) {
public TransactionData(String name, Transaction transaction) {
this.name = name;
this.transaction = transaction;
}
@@ -58,10 +61,6 @@ public class TransactionData {
return name;
}
public void setName(String name) {
this.name = name;
}
public PSBT getPsbt() {
return psbt;
}
@@ -150,7 +149,11 @@ public class TransactionData {
this.signingWallet.set(wallet);
}
public ObservableList<Keystore> getSignedKeystores() {
return signedKeystores;
public ObservableMap<TransactionSignature, Keystore> getSignatureKeystoreMap() {
return signatureKeystoreMap;
}
public Collection<Keystore> getSignedKeystores() {
return signatureKeystoreMap.values();
}
}
@@ -2,17 +2,18 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.scene.Node;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -23,6 +24,10 @@ public abstract class TransactionForm {
this.txdata = txdata;
}
public TransactionData getTransactionData() {
return txdata;
}
public Transaction getTransaction() {
return txdata.getTransaction();
}
@@ -79,7 +84,11 @@ public abstract class TransactionForm {
txdata.setSigningWallet(signingWallet);
}
public ObservableList<Keystore> getSignedKeystores() {
public ObservableMap<TransactionSignature, Keystore> getSignatureKeystoreMap() {
return txdata.getSignatureKeystoreMap();
}
public Collection<Keystore> getSignedKeystores() {
return txdata.getSignedKeystores();
}
@@ -1,9 +1,13 @@
package com.sparrowwallet.sparrow.transaction;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.event.TransactionTabsClosedEvent;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.PieChart;
@@ -18,6 +22,8 @@ import java.util.List;
public abstract class TransactionFormController extends BaseController {
private static final int MAX_PIE_SEGMENTS = 200;
protected abstract TransactionForm getTransactionForm();
protected void addPieData(PieChart pie, List<TransactionOutput> outputs) {
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableArrayList();
@@ -64,6 +70,15 @@ public abstract class TransactionFormController extends BaseController {
});
}
@Subscribe
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) {
EventManager.get().unregister(this);
}
}
}
public static class TransactionReferenceContextMenu extends ContextMenu {
public TransactionReferenceContextMenu(String reference) {
MenuItem referenceItem = new MenuItem("Copy Reference");
@@ -19,6 +19,7 @@ import java.util.Objects;
public class UR {
public static final String UR_PREFIX = "ur";
public static final String BYTES_TYPE = "bytes";
public static final String CRYPTO_PSBT_TYPE = "crypto-psbt";
private final String type;
private final byte[] data;
@@ -55,13 +55,17 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
return (type.equals(Type.INPUT) ? "Spent by input " : "Received from output ") +
getHashIndex().getHash().toString().substring(0, 8) + "..:" +
getHashIndex().getIndex() +
" on " + DateLabel.getShortDateFormat(getHashIndex().getDate());
(getHashIndex().getHeight() <= 0 ? " (Unconfirmed)" : " on " + DateLabel.getShortDateFormat(getHashIndex().getDate()));
}
public boolean isSpent() {
return getType().equals(HashIndexEntry.Type.INPUT) || getHashIndex().getSpentBy() != null;
}
public boolean isSpendable() {
return !isSpent() && (hashIndex.getHeight() > 0 || wallet.allInputsFromWallet(hashIndex.getHash()));
}
@Override
public Long getValue() {
return hashIndex.getValue();
@@ -94,7 +98,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
}
if(getHashIndex().getHeight() != o.getHashIndex().getHeight()) {
return (o.getHashIndex().getHeight() > 0 ? o.getHashIndex().getHeight() : Integer.MAX_VALUE) - (getHashIndex().getHeight() > 0 ? getHashIndex().getHeight() : Integer.MAX_VALUE);
return o.getHashIndex().getComparisonHeight() - getHashIndex().getComparisonHeight();
}
return (int)o.getHashIndex().getIndex() - (int)getHashIndex().getIndex();
@@ -98,6 +98,7 @@ public class KeystoreController extends WalletFormController implements Initiali
if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString());
setXpubTooltip(keystore.getExtendedPublicKey());
}
if(keystore.getKeyDerivation() != null) {
@@ -123,12 +124,26 @@ public class KeystoreController extends WalletFormController implements Initiali
});
xpub.textProperty().addListener((observable, oldValue, newValue) -> {
if(ExtendedKey.isValid(newValue)) {
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(newValue));
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(newValue);
setXpubTooltip(extendedKey);
keystore.setExtendedPublicKey(extendedKey);
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_XPUB));
} else {
xpub.setTooltip(null);
}
});
}
private void setXpubTooltip(ExtendedKey extendedKey) {
ExtendedKey.Header header = ExtendedKey.Header.fromScriptType(walletForm.getWallet().getScriptType(), false);
if(header != ExtendedKey.Header.xpub) {
Tooltip tooltip = new Tooltip(extendedKey.getExtendedKey(header));
xpub.setTooltip(tooltip);
} else {
xpub.setTooltip(null);
}
}
public void selectSource(ActionEvent event) {
keystoreSourceToggleGroup.selectToggle(null);
ToggleButton sourceButton = (ToggleButton)event.getSource();
@@ -180,6 +195,7 @@ public class KeystoreController extends WalletFormController implements Initiali
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED);
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
boolean editable = (keystore.getSource() == KeystoreSource.SW_WATCH);
fingerprint.setEditable(editable);
@@ -287,6 +303,9 @@ public class KeystoreController extends WalletFormController implements Initiali
derivation.setText(derivationPath + " ");
derivation.setText(derivationPath);
}
if(keystore.getExtendedPublicKey() != null) {
setXpubTooltip(keystore.getExtendedPublicKey());
}
}
}
}
@@ -101,7 +101,7 @@ public class ReceiveController extends WalletFormController implements Initializ
this.currentEntry = nodeEntry;
address.setText(nodeEntry.getAddress().toString());
label.textProperty().bindBidirectional(nodeEntry.labelProperty());
derivationPath.setText(nodeEntry.getNode().getDerivationPath());
updateDerivationPath(nodeEntry);
updateLastUsed();
@@ -119,6 +119,23 @@ public class ReceiveController extends WalletFormController implements Initializ
updateDisplayAddress(AppController.getDevices());
}
private void updateDerivationPath(NodeEntry nodeEntry) {
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
boolean singleDerivationPath = true;
for(Keystore keystore : getWalletForm().getWallet().getKeystores()) {
if(!keystore.getKeyDerivation().getDerivationPath().equals(firstDerivation.getDerivationPath())) {
singleDerivationPath = false;
break;
}
}
if(singleDerivationPath) {
derivationPath.setText(firstDerivation.extend(nodeEntry.getNode().getDerivation()).getDerivationPath());
} else {
derivationPath.setText(nodeEntry.getNode().getDerivationPath().replace("m", "multi"));
}
}
private void updateLastUsed() {
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
if(AppController.isOnline() && currentOutputs.isEmpty()) {
@@ -127,7 +144,7 @@ public class ReceiveController extends WalletFormController implements Initializ
} else if(!currentOutputs.isEmpty()) {
long count = currentOutputs.size();
BlockTransactionHashIndex lastUsedReference = currentOutputs.stream().skip(count - 1).findFirst().get();
lastUsed.setText(DATE_FORMAT.format(lastUsedReference.getDate()));
lastUsed.setText(lastUsedReference.getHeight() <= 0 ? "Unconfirmed Transaction" : DATE_FORMAT.format(lastUsedReference.getDate()));
lastUsed.setGraphic(getWarningGlyph());
} else {
lastUsed.setText("Unknown");
@@ -194,21 +211,21 @@ public class ReceiveController extends WalletFormController implements Initializ
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) {
if(possibleDevices.size() > 1 || possibleDevices.get(0).getNeedsPinSent() || possibleDevices.get(0).getNeedsPassphraseSent()) {
DeviceAddressDialog dlg = new DeviceAddressDialog(possibleDevices.size() == 1 ? List.of(possibleDevices.get(0)) : null, wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
dlg.showAndWait();
} else {
Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), fullDerivation.getDerivationPath());
displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(null, wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
dlg.showAndWait();
});
});
displayAddressService.start();
}
} else {
DeviceAddressDialog dlg = new DeviceAddressDialog(null, wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
dlg.showAndWait();
}
}
@@ -35,6 +35,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.stream.Collectors;
@@ -73,6 +74,9 @@ public class SendController extends WalletFormController implements Initializabl
@FXML
private ComboBox<BitcoinUnit> feeAmountUnit;
@FXML
private FiatLabel fiatFeeAmount;
@FXML
private FeeRatesChart feeRatesChart;
@@ -115,6 +119,12 @@ public class SendController extends WalletFormController implements Initializabl
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
userFeeSet.set(true);
if(newValue.isEmpty()) {
fiatFeeAmount.setText("");
} else {
setFiatFeeAmount(AppController.getFiatCurrencyExchangeRate(), getFeeValueSats());
}
setTargetBlocks(getTargetBlocks());
updateTransaction();
}
@@ -171,7 +181,7 @@ public class SendController extends WalletFormController implements Initializabl
amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
Long value = getRecipientValueSats(oldValue);
if(value != null) {
DecimalFormat df = new DecimalFormat("#.#");
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
df.setMaximumFractionDigits(8);
amount.setText(df.format(newValue.getValue(value)));
}
@@ -256,6 +266,7 @@ public class SendController extends WalletFormController implements Initializabl
if(walletTransaction != null) {
if(getRecipientValueSats() == null || walletTransaction.getRecipientAmount() != getRecipientValueSats()) {
setRecipientValueSats(walletTransaction.getRecipientAmount());
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), walletTransaction.getRecipientAmount());
}
double feeRate = (double)walletTransaction.getFee() / walletTransaction.getTransaction().getVirtualSize();
@@ -365,7 +376,7 @@ public class SendController extends WalletFormController implements Initializabl
private void setRecipientValueSats(long recipientValue) {
amount.textProperty().removeListener(amountListener);
DecimalFormat df = new DecimalFormat("#.#");
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
df.setMaximumFractionDigits(8);
amount.setText(df.format(amountUnit.getValue().getValue(recipientValue)));
amount.textProperty().addListener(amountListener);
@@ -386,10 +397,11 @@ public class SendController extends WalletFormController implements Initializabl
private void setFeeValueSats(long feeValue) {
fee.textProperty().removeListener(feeListener);
DecimalFormat df = new DecimalFormat("#.#");
DecimalFormat df = new DecimalFormat("#.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
df.setMaximumFractionDigits(8);
fee.setText(df.format(feeAmountUnit.getValue().getValue(feeValue)));
fee.textProperty().addListener(feeListener);
setFiatFeeAmount(AppController.getFiatCurrencyExchangeRate(), feeValue);
}
private Integer getTargetBlocks() {
@@ -468,6 +480,12 @@ public class SendController extends WalletFormController implements Initializabl
}
}
private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) {
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
fiatFeeAmount.set(currencyRate, amount);
}
}
private long getRecipientDustThreshold() {
Address address;
try {
@@ -480,6 +498,25 @@ public class SendController extends WalletFormController implements Initializabl
return address.getScriptType().getDustThreshold(txOutput, getFeeRate());
}
public void scanQrAddress(ActionEvent event) {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.uri != null) {
if(result.uri.getAddress() != null) {
address.setText(result.uri.getAddress().toString());
}
if(result.uri.getLabel() != null) {
label.setText(result.uri.getLabel());
}
if(result.uri.getAmount() != null) {
setRecipientValueSats(result.uri.getAmount());
}
}
}
}
public void clear(ActionEvent event) {
address.setText("");
label.setText("");
@@ -488,10 +525,14 @@ public class SendController extends WalletFormController implements Initializabl
amount.setText("");
amount.textProperty().addListener(amountListener);
fiatAmount.setText("");
fee.textProperty().removeListener(feeListener);
fee.setText("");
fee.textProperty().addListener(feeListener);
fiatFeeAmount.setText("");
userFeeSet.set(false);
targetBlocks.setValue(4);
utxoSelectorProperty.setValue(null);
@@ -569,5 +610,6 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
}
}
@@ -286,7 +286,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
if(password.get().length() == 0) {
if(password.get().length() == 0 && requirement != WalletPasswordDialog.PasswordRequirement.UPDATE_SET) {
try {
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
walletForm.saveAndRefresh();
@@ -339,6 +339,9 @@ public class SettingsController extends WalletFormController implements Initiali
EventManager.get().post(new StorageEvent(walletForm.getWalletFile(), TimedEvent.Action.START, "Encrypting wallet..."));
keyDerivationService.start();
}
} else {
revert.setDisable(false);
apply.setDisable(false);
}
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
@@ -31,10 +32,51 @@ public class SettingsWalletForm extends WalletForm {
@Override
public void saveAndRefresh() throws IOException {
//TODO: Detect trivial changes and don't clear everything
walletCopy.clearNodes();
boolean refreshAll = changesScriptHashes(wallet, walletCopy);
if(refreshAll) {
walletCopy.clearNodes();
}
wallet = walletCopy.copy();
save();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, getWalletFile()));
if(refreshAll) {
EventManager.get().post(new WalletSettingsChangedEvent(wallet, getWalletFile()));
}
}
private boolean changesScriptHashes(Wallet original, Wallet changed) {
if(!original.isValid() || !changed.isValid()) {
return true;
}
if(original.getPolicyType() != changed.getPolicyType()) {
return true;
}
if(original.getScriptType() != changed.getScriptType()) {
return true;
}
//TODO: Determine if Miniscript has changed for custom policies
if(original.getKeystores().size() != changed.getKeystores().size()) {
return true;
}
for(int i = 0; i < original.getKeystores().size(); i++) {
Keystore originalKeystore = original.getKeystores().get(i);
Keystore changedKeystore = changed.getKeystores().get(i);
if(!originalKeystore.getKeyDerivation().equals(changedKeystore.getKeyDerivation())) {
return true;
}
if(!originalKeystore.getExtendedPublicKey().equals(changedKeystore.getExtendedPublicKey())) {
return true;
}
}
return false;
}
}
@@ -7,8 +7,10 @@ import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.LongProperty;
@@ -204,4 +206,17 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
}
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm().getWallet() == wallet) {
try {
EventManager.get().unregister(this);
} catch(IllegalArgumentException e) {
//Safe to ignore
}
}
}
}
}
@@ -14,7 +14,6 @@ import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TreeItem;
import javafx.scene.input.MouseEvent;
import java.net.URL;
import java.util.ResourceBundle;
@@ -47,8 +46,10 @@ public class TransactionsController extends WalletFormController implements Init
transactionsTable.initialize(walletTransactionsEntry);
balance.valueProperty().addListener((observable, oldValue, newValue) -> {
setFiatBalance(AppController.getFiatCurrencyExchangeRate(), newValue.longValue());
});
balance.setValue(walletTransactionsEntry.getBalance());
setFiatBalance(AppController.getFiatCurrencyExchangeRate(), walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.initialize(walletTransactionsEntry);
@@ -1,19 +1,24 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.UtxosChart;
import com.sparrowwallet.sparrow.control.UtxosTreeTable;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
@@ -22,6 +27,9 @@ public class UtxosController extends WalletFormController implements Initializab
@FXML
private UtxosTreeTable utxosTable;
@FXML
private Button sendSelected;
@FXML
private UtxosChart utxosChart;
@@ -34,13 +42,53 @@ public class UtxosController extends WalletFormController implements Initializab
public void initializeView() {
utxosTable.initialize(getWalletForm().getWalletUtxosEntry());
utxosChart.initialize(getWalletForm().getWalletUtxosEntry());
sendSelected.setDisable(true);
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue()).collect(Collectors.toList());
utxosChart.select(selectedEntries);
updateSendSelected(Config.get().getBitcoinUnit());
});
}
private void updateSendSelected(BitcoinUnit unit) {
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> tp.getTreeItem().getValue())
.filter(entry -> ((HashIndexEntry)entry).isSpendable())
.collect(Collectors.toList());
sendSelected.setDisable(selectedEntries.isEmpty());
long selectedTotal = selectedEntries.stream().mapToLong(Entry::getValue).sum();
if(selectedTotal > 0) {
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (selectedTotal >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}
if(unit.equals(BitcoinUnit.BTC)) {
sendSelected.setText("Send Selected (" + CoinLabel.getBTCFormat().format((double)selectedTotal / Transaction.SATOSHIS_PER_BITCOIN) + " BTC)");
} else {
sendSelected.setText("Send Selected (" + String.format(Locale.ENGLISH, "%,d", selectedTotal) + " sats)");
}
} else {
sendSelected.setText("Send Selected");
}
}
public void sendSelected(ActionEvent event) {
List<HashIndexEntry> utxoEntries = utxosTable.getSelectionModel().getSelectedCells().stream()
.map(tp -> tp.getTreeItem().getValue())
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
.collect(Collectors.toList());
EventManager.get().post(new SendActionEvent(utxoEntries));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(utxoEntries)));
}
public void clear(ActionEvent event) {
utxosTable.getSelectionModel().clearSelection();
}
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
@@ -75,5 +123,6 @@ public class UtxosController extends WalletFormController implements Initializab
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
utxosTable.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
utxosChart.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
updateSendSelected(event.getBitcoinUnit());
}
}
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
@@ -71,7 +72,7 @@ public class WalletForm {
updateWallet(previousWallet, blockHeight);
});
historyService.setOnFailed(workerStateEvent -> {
workerStateEvent.getSource().getException().printStackTrace();
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
});
historyService.start();
}
@@ -199,7 +200,10 @@ public class WalletForm {
@Subscribe
public void newBlock(NewBlockEvent event) {
updateWallet(wallet.copy(), event.getHeight());
//Check if wallet is valid to avoid saving wallets in initial setup
if(wallet.isValid()) {
updateWallet(wallet.copy(), event.getHeight());
}
}
@Subscribe
@@ -213,4 +217,13 @@ public class WalletForm {
refreshHistory(AppController.getCurrentBlockHeight());
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm() == this) {
EventManager.get().unregister(this);
}
}
}
}
@@ -1,6 +1,10 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
public abstract class WalletFormController extends BaseController {
public WalletForm walletForm;
@@ -15,4 +19,15 @@ public abstract class WalletFormController extends BaseController {
}
public abstract void initializeView();
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm() == walletForm) {
EventManager.get().unregister(this);
} else if(walletForm instanceof SettingsWalletForm && tabData.getStorage() == walletForm.getStorage()) {
EventManager.get().unregister(this);
}
}
}
}
BIN
View File
Binary file not shown.
@@ -0,0 +1,15 @@
.title-area {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 10 25 10 25;
-fx-border-width: 0px 0px 1px 0px;
-fx-border-color: #e5e5e6;
}
.title-label {
-fx-font-size: 20px;
}
.content-area, .button-area {
-fx-padding: 10 25 25 25;
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<StackPane prefHeight="420.0" prefWidth="600.0" stylesheets="@about.css" fx:controller="com.sparrowwallet.sparrow.AboutController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<VBox spacing="20">
<HBox styleClass="title-area">
<HBox alignment="CENTER_LEFT">
<Label fx:id="title" text="Sparrow" styleClass="title-label" />
</HBox>
<Region HBox.hgrow="ALWAYS"/>
<ImageView AnchorPane.rightAnchor="0">
<Image url="/image/sparrow-small.png" requestedWidth="50" requestedHeight="50" smooth="false" />
</ImageView>
</HBox>
<VBox spacing="10" styleClass="content-area">
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact will a full understanding of your money." wrapText="true" />
<Label text="Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator." wrapText="true" />
<Label text="For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy." wrapText="true" />
</VBox>
<HBox styleClass="button-area" alignment="BOTTOM_RIGHT" VBox.vgrow="SOMETIMES">
<Button text="Done" onAction="#close" />
</HBox>
</VBox>
</StackPane>
@@ -8,7 +8,7 @@
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="750.0" prefWidth="1000.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="770.0" prefWidth="1000.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<children>
<MenuBar useSystemMenuBar="true">
<menus>
@@ -33,7 +33,6 @@
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Preferences..." onAction="#openPreferences"/>
<SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Close Tab" onAction="#closeTab"/>
<SeparatorMenuItem styleClass="macHide" />
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Quit" onAction="#quit"/>
</items>
</Menu>
@@ -64,7 +63,8 @@
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem styleClass="macHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
</Menu>
</menus>
</MenuBar>
@@ -76,7 +76,11 @@
<StatusBar fx:id="statusBar" text="" minHeight="36">
<rightItems>
<UnlabeledToggleSwitch fx:id="serverToggle" />
<UnlabeledToggleSwitch fx:id="serverToggle">
<tooltip>
<Tooltip text="Disconnected" />
</tooltip>
</UnlabeledToggleSwitch>
</rightItems>
</StatusBar>
</children>

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