mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cdffec060 | ||
|
|
ed9bc882b7 | ||
|
|
5054ab9564 | ||
|
|
7751f940f2 | ||
|
|
3f68ca43c2 | ||
|
|
68239e9472 | ||
|
|
10edf31e13 | ||
|
|
61c3c4a3ac | ||
|
|
5d6bb5e4f9 | ||
|
|
b808222faa | ||
|
|
4d7d51f3f8 | ||
|
|
546076ea48 | ||
|
|
2bebb0925e | ||
|
|
47855228d3 | ||
|
|
a6e41e3f9d | ||
|
|
04722204a6 | ||
|
|
452afe4731 | ||
|
|
af36d7b5a5 | ||
|
|
7e225e794f | ||
|
|
ffce18919f | ||
|
|
ddcabf721e | ||
|
|
7eb4b9c61d | ||
|
|
6dca03f8b4 | ||
|
|
135c1d1719 | ||
|
|
c9a4fb4ec4 | ||
|
|
06957b12e6 | ||
|
|
d1fbe94d90 | ||
|
|
1fea119173 | ||
|
|
1ac264600a | ||
|
|
00db59c44f | ||
|
|
2c27f702bf | ||
|
|
3c2dfed96d | ||
|
|
eb23cc8e61 | ||
|
|
fa0c74f06b | ||
|
|
ba017b2fcb | ||
|
|
0a094332d2 | ||
|
|
2cca6049be | ||
|
|
5be1f066df | ||
|
|
d519c033ac | ||
|
|
f8d15f73f6 | ||
|
|
350bddac84 | ||
|
|
d1326cb621 | ||
|
|
ae0e5894ca | ||
|
|
1a931c72bd | ||
|
|
92db6d9396 | ||
|
|
5fd59cc68a | ||
|
|
ea9a310b22 | ||
|
|
ee22d4a81d | ||
|
|
c2fb6bcfee | ||
|
|
8cde670efd | ||
|
|
f95cb67912 | ||
|
|
750f6483cb | ||
|
|
53a590140d | ||
|
|
189ac75467 | ||
|
|
fa082e892f | ||
|
|
c814b8e350 | ||
|
|
2b48b4cd6e | ||
|
|
d49f85fe5b | ||
|
|
af525797ff | ||
|
|
e9e10de266 | ||
|
|
ab5fdef919 | ||
|
|
f723d20c9a | ||
|
|
03c488cf4d | ||
|
|
ca64a1d307 | ||
|
|
4c8f7bb711 | ||
|
|
77dd8ca5d7 | ||
|
|
d5aba35184 | ||
|
|
30e2ab5e2a | ||
|
|
e93fcf7fbd | ||
|
|
198672a06e | ||
|
|
133d771c09 | ||
|
|
9a80065605 | ||
|
|
ccf4e98968 | ||
|
|
df15129f64 | ||
|
|
e878648587 | ||
|
|
75bf63d936 | ||
|
|
304bdd5c48 | ||
|
|
e1934c99bc |
@@ -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
@@ -1,3 +1,3 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = git@github.com:craigraw/drongo.git
|
||||
url = git@github.com:sparrowwallet/drongo.git
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"'`
|
||||

|
||||
|
||||
`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
@@ -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
Vendored
BIN
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);
|
||||
|
||||
+42
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
BIN
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
Reference in New Issue
Block a user