mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6d8028f8 | ||
|
|
5d404f8c30 | ||
|
|
94662922a8 | ||
|
|
582065e7f0 | ||
|
|
ccead92388 | ||
|
|
2b47740539 | ||
|
|
668dd88436 | ||
|
|
76820377ae | ||
|
|
6a1c3fa3da | ||
|
|
dfa781e16c | ||
|
|
722fd84ec1 | ||
|
|
39d1e686f4 | ||
|
|
7461b47466 | ||
|
|
572e451279 | ||
|
|
88dfde3c67 | ||
|
|
64c3db3656 | ||
|
|
f0ee99b78b | ||
|
|
48336bb894 | ||
|
|
c804737563 | ||
|
|
d395c7897e | ||
|
|
6e2e5d7a9c | ||
|
|
99a39aa83f | ||
|
|
2ca8b91283 | ||
|
|
94960567b5 | ||
|
|
e14349a645 | ||
|
|
ee7b741a69 | ||
|
|
02c8415a7b | ||
|
|
f305e0bdc2 | ||
|
|
d8b7938a63 | ||
|
|
44ac7f3c0e | ||
|
|
2b55b5feb3 | ||
|
|
43bf6ab265 | ||
|
|
80b7ee803b | ||
|
|
8e23bd64c7 | ||
|
|
bf6fbebd9e | ||
|
|
0b9052dee9 | ||
|
|
6460cb88a5 | ||
|
|
90ac786707 | ||
|
|
c86006335b | ||
|
|
a444e2af2f | ||
|
|
bcd2c966a5 | ||
|
|
f826c2d423 | ||
|
|
37bb1c5d97 | ||
|
|
68da8e6027 | ||
|
|
87fe0accd5 | ||
|
|
8d413e839c | ||
|
|
1392199f5c | ||
|
|
dc33b64a9c | ||
|
|
1b220c72ea | ||
|
|
bcf6f77340 | ||
|
|
0aac8bbea7 | ||
|
|
e1acaa8a78 | ||
|
|
13a486597c | ||
|
|
389ce2180d | ||
|
|
a7a3ad012c | ||
|
|
9f5eb41d90 | ||
|
|
3a4d385560 | ||
|
|
d3881f76b9 | ||
|
|
be5a48ce71 | ||
|
|
c8cd2d1cd9 | ||
|
|
5789e1c215 | ||
|
|
f48fb610e8 | ||
|
|
3aeba79268 | ||
|
|
4a030e9b6e | ||
|
|
2cf653702c | ||
|
|
ac438ec023 | ||
|
|
ee9247c066 | ||
|
|
4ea5ec6c0a | ||
|
|
63a9a798f3 | ||
|
|
194f0f5c1b | ||
|
|
bda5c1a709 | ||
|
|
32327928b5 | ||
|
|
b448927a6e | ||
|
|
ff578c938e | ||
|
|
998100505d | ||
|
|
a893b37bb0 | ||
|
|
4c58ebf138 | ||
|
|
76e148a107 | ||
|
|
20f5e4d8db | ||
|
|
5cd15f21ad | ||
|
|
b50cf77f3b | ||
|
|
5e8148e5b9 | ||
|
|
fa5ac7917b | ||
|
|
eda712e269 | ||
|
|
3c6adafc15 | ||
|
|
639e145672 | ||
|
|
eb8d66bf25 | ||
|
|
3a885b3a28 | ||
|
|
7e93b6b198 | ||
|
|
f949bcc1cf | ||
|
|
9e5937d2fe | ||
|
|
82a8f08440 | ||
|
|
c806959199 | ||
|
|
f7fb62fcea | ||
|
|
ea2783e51b | ||
|
|
ff7a35e68b | ||
|
|
c68cda5b2f | ||
|
|
2cb667a671 | ||
|
|
eb09076604 | ||
|
|
32bc9f6a99 | ||
|
|
dc8a8dd857 | ||
|
|
d5a26fdf5d | ||
|
|
e48e40da0d | ||
|
|
e878c4ea78 | ||
|
|
ead8fa2207 | ||
|
|
9a395f5dd1 | ||
|
|
a99114cc6f | ||
|
|
ea36c63ebb | ||
|
|
77d3c848f9 | ||
|
|
f22312e04f | ||
|
|
b9db4421df | ||
|
|
b7aa73e99f | ||
|
|
4bad46c9e3 | ||
|
|
084a3c76af | ||
|
|
2e0ca1b4fa | ||
|
|
4bc446724d | ||
|
|
0c56f5a9a1 | ||
|
|
38b27bb091 | ||
|
|
b8c3bf1bea | ||
|
|
e65f1ef3cc | ||
|
|
f25d2618b6 | ||
|
|
0adf94bf62 | ||
|
|
7d7c5e5064 | ||
|
|
10e4d8843d | ||
|
|
6e10784e49 | ||
|
|
885efd7985 | ||
|
|
ec9534f8ae | ||
|
|
6c90a7e0ba | ||
|
|
9dd52f120f | ||
|
|
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 |
@@ -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,19 +0,0 @@
|
||||
BSD-2-Clause Plus Patent License
|
||||
|
||||
SPDX-License-Identifier: [BSD-2-Clause-Patent](https://spdx.org/licenses/BSD-2-Clause-Patent.html)
|
||||
|
||||
Copyright © 2020 Blockchain Commons, LLC
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:
|
||||
|
||||
(a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of contributors, in source or binary form) alone; or
|
||||
(b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.
|
||||
Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this license, whether expressly, by implication, estoppel or otherwise.
|
||||
|
||||
DISCLAIMER
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,11 +1,82 @@
|
||||
# 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`
|
||||
|
||||
When updating to the latest HEAD
|
||||
|
||||
`git pull --recurse-submodules`
|
||||
|
||||
All jar files created are reproducible builds.
|
||||
|
||||
## Running
|
||||
|
||||
If you prefer to run Sparrow directly from source, it can be launched with
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 14 must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
Sparrow has a number of command line options, for example to change it's home folder or use testnet:
|
||||
|
||||
```
|
||||
./sparrow -h
|
||||
|
||||
Usage: sparrow [options]
|
||||
Options:
|
||||
--dir, -d
|
||||
Path to Sparrow home folder
|
||||
--help, -h
|
||||
Show usage
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet or regtest) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
A final fallback which can be useful when running the Sparrow binary is to create a file called ``network-testnet`` in the Sparrow home folder (see below) to configure the testnet network.
|
||||
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to running on testnet configuration as well.
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores it's mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
|
||||
Platform | Location
|
||||
-------- | --------
|
||||
OSX | ~/.sparrow
|
||||
Linux | ~/.sparrow
|
||||
Windows | %APPDATA%/Sparrow
|
||||
|
||||
Testnet and regtest configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab above to report an issue. If possible, look in the sparrow.log file in the configuration directory for information helpful in debugging.
|
||||
|
||||
## License
|
||||
|
||||
Sparrow is licensed under the Apache 2 software licence.
|
||||
|
||||
## Credit
|
||||
|
||||

|
||||
|
||||
Sparrow Wallet uses the [Yourkit Java Profiler](https://www.yourkit.com/java/profiler/) to profile and improve performance.
|
||||
YourKit supports open source projects with useful tools for monitoring and profiling Java and .NET applications.
|
||||
|
||||
+60
-21
@@ -1,23 +1,35 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.8'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.9'
|
||||
id 'org.kordamp.gradle.jdeps' version '0.9.0'
|
||||
id 'org.beryx.jlink' version '2.17.4'
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
}
|
||||
|
||||
group 'com.sparrowwallet'
|
||||
version '1.0-SNAPSHOT'
|
||||
def sparrowVersion = '0.9.9'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
osName = "osx"
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.9
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'http://maven.openimaj.org/' }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "14"
|
||||
version = "15"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -39,11 +51,14 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20200803')
|
||||
implementation('com.sparrowwallet:hummingbird:1.5.2')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.0.2' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -62,13 +77,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/native").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 +105,14 @@ 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",
|
||||
"--add-opens=java.base/java.net=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 {
|
||||
@@ -93,6 +124,7 @@ jlink {
|
||||
requires 'javafx.base'
|
||||
requires 'com.fasterxml.jackson.databind'
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
|
||||
@@ -111,25 +143,32 @@ 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-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
jpackage {
|
||||
imageName = "Sparrow"
|
||||
installerName = "Sparrow"
|
||||
appVersion = "0.9.1"
|
||||
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/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
|
||||
installerType = "dmg"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule drongo updated: 446eac3483...0567409742
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
if [ -n "$args" ]
|
||||
then
|
||||
./gradlew run --args="$args"
|
||||
else
|
||||
./gradlew run
|
||||
fi
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
set ARGS=%*
|
||||
|
||||
if "%ARGS%" == "" (
|
||||
gradlew.bat run
|
||||
) else (
|
||||
gradlew.bat run --args="%ARGS%"
|
||||
)
|
||||
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.9</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 |
@@ -1,11 +1,20 @@
|
||||
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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,488 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.net.VersionCheckService;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AppServices {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
|
||||
|
||||
private static final int SERVER_PING_PERIOD = 1 * 60 * 1000;
|
||||
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
|
||||
private static final int RATES_PERIOD = 5 * 60 * 1000;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final MainApp application;
|
||||
|
||||
private final Map<Window, Map<Wallet, Storage>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private ExchangeSource.RatesService ratesService;
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
||||
|
||||
private VersionCheckService versionCheckService;
|
||||
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||
|
||||
private static List<Device> devices;
|
||||
|
||||
private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
|
||||
|
||||
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
|
||||
Config.get().setMode(online ? Mode.ONLINE : Mode.OFFLINE);
|
||||
if(online) {
|
||||
restartService(connectionService);
|
||||
|
||||
if(ratesService.getExchangeSource() != ExchangeSource.NONE) {
|
||||
restartService(ratesService);
|
||||
}
|
||||
|
||||
if(Config.get().isCheckNewVersions()) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
} else {
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void restartService(ScheduledService<?> service) {
|
||||
if(service.isRunning()) {
|
||||
service.cancel();
|
||||
}
|
||||
|
||||
if(service.getState() == Worker.State.CANCELLED) {
|
||||
service.reset();
|
||||
}
|
||||
|
||||
if(!service.isRunning()) {
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
|
||||
public AppServices(MainApp application) {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Config config = Config.get();
|
||||
connectionService = createConnectionService();
|
||||
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
|
||||
connectionService.start();
|
||||
}
|
||||
|
||||
ExchangeSource source = config.getExchangeSource() != null ? config.getExchangeSource() : DEFAULT_EXCHANGE_SOURCE;
|
||||
Currency currency = config.getFiatCurrency() != null ? config.getFiatCurrency() : DEFAULT_FIAT_CURRENCY;
|
||||
ratesService = createRatesService(source, currency);
|
||||
if(config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) {
|
||||
ratesService.start();
|
||||
}
|
||||
|
||||
versionCheckService = createVersionCheckService();
|
||||
if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) {
|
||||
versionCheckService.start();
|
||||
}
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
}
|
||||
|
||||
private ElectrumServer.ConnectionService createConnectionService() {
|
||||
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
|
||||
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
|
||||
connectionService.setRestartOnFailure(true);
|
||||
|
||||
EventManager.get().register(connectionService);
|
||||
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(connectionService.isRunning()) {
|
||||
EventManager.get().post(new StatusEvent(newValue));
|
||||
}
|
||||
});
|
||||
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
onlineProperty.removeListener(onlineServicesListener);
|
||||
onlineProperty.setValue(true);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
if(connectionService.getValue() != null) {
|
||||
EventManager.get().post(connectionService.getValue());
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
//Close connection here to create a new transport next time we try
|
||||
connectionService.resetConnection();
|
||||
|
||||
onlineProperty.removeListener(onlineServicesListener);
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
return connectionService;
|
||||
}
|
||||
|
||||
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(exchangeSource, currency);
|
||||
ratesService.setPeriod(new Duration(RATES_PERIOD));
|
||||
ratesService.setRestartOnFailure(true);
|
||||
|
||||
ratesService.setOnSucceeded(successEvent -> {
|
||||
EventManager.get().post(ratesService.getValue());
|
||||
});
|
||||
|
||||
return ratesService;
|
||||
}
|
||||
|
||||
private VersionCheckService createVersionCheckService() {
|
||||
VersionCheckService versionCheckService = new VersionCheckService();
|
||||
versionCheckService.setDelay(Duration.seconds(10));
|
||||
versionCheckService.setPeriod(Duration.hours(VERSION_CHECK_PERIOD_HOURS));
|
||||
versionCheckService.setRestartOnFailure(true);
|
||||
|
||||
versionCheckService.setOnSucceeded(successEvent -> {
|
||||
VersionUpdatedEvent event = versionCheckService.getValue();
|
||||
if(event != null) {
|
||||
EventManager.get().post(event);
|
||||
}
|
||||
});
|
||||
|
||||
return versionCheckService;
|
||||
}
|
||||
|
||||
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
|
||||
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
|
||||
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
|
||||
//Null devices are returned if the app is currently prompting for a pin. Otherwise, the enumerate clears the pin screen
|
||||
if(devices != null) {
|
||||
//If another instance of HWI is currently accessing the usb interface, HWI returns empty device models. Ignore this run if that happens
|
||||
List<Device> validDevices = devices.stream().filter(device -> device.getModel() != null).collect(Collectors.toList());
|
||||
if(validDevices.size() == devices.size()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return enumerateService;
|
||||
}
|
||||
|
||||
static void initialize(MainApp application) {
|
||||
INSTANCE = new AppServices(application);
|
||||
}
|
||||
|
||||
public static AppServices get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static AppController newAppWindow(Stage stage) {
|
||||
try {
|
||||
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
|
||||
Parent root = appLoader.load();
|
||||
AppController appController = appLoader.getController();
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
scene.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
|
||||
stage.setTitle("Sparrow");
|
||||
stage.setMinWidth(650);
|
||||
stage.setMinHeight(800);
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
|
||||
|
||||
appController.initializeView();
|
||||
stage.show();
|
||||
return appController;
|
||||
} catch(IOException e) {
|
||||
log.error("Could not load app FXML", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public MainApp getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets(Window window) {
|
||||
return walletWindows.get(window);
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(Storage storage) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().values().stream().anyMatch(storage1 -> storage1.getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
public Window getWindowForPSBT(PSBT psbt) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().keySet().stream().anyMatch(wallet -> wallet.canSign(psbt))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
public double getWalletWindowMaxX() {
|
||||
return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d);
|
||||
}
|
||||
|
||||
public static boolean isOnline() {
|
||||
return onlineProperty.get();
|
||||
}
|
||||
|
||||
public static BooleanProperty onlineProperty() {
|
||||
return onlineProperty;
|
||||
}
|
||||
|
||||
public static Integer getCurrentBlockHeight() {
|
||||
return currentBlockHeight;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
return mempoolHistogram;
|
||||
}
|
||||
|
||||
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
|
||||
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||
if(mempoolHistogram.isEmpty()) {
|
||||
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
}
|
||||
|
||||
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||
return fiatCurrencyExchangeRate;
|
||||
}
|
||||
|
||||
public static List<Device> getDevices() {
|
||||
return devices == null ? new ArrayList<>() : devices;
|
||||
}
|
||||
|
||||
public static BitcoinURI getPayjoinURI(Address address) {
|
||||
return payjoinURIs.get(address);
|
||||
}
|
||||
|
||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||
throw new IllegalArgumentException("Not a payjoin URI");
|
||||
}
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
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(AppServices.class.getResourceAsStream("/image/sparrow.png")));
|
||||
|
||||
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
String banner = event.getServerBanner();
|
||||
String status = "Connected to " + Config.get().getElectrumServer() + " at height " + event.getBlockHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
|
||||
String status = "Connection error: " + reason;
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void usbDevicesFound(UsbDeviceEvent event) {
|
||||
devices = Collections.unmodifiableList(event.getDevices());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
currentBlockHeight = event.getHeight();
|
||||
String status = "Updating to new block height " + event.getHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(feeRatesService.getValue());
|
||||
});
|
||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||
feeRatesService.start();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||
ratesService.cancel();
|
||||
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.getExchangeSource() != ExchangeSource.NONE) {
|
||||
ratesService = createRatesService(event.getExchangeSource(), event.getCurrency());
|
||||
ratesService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||
fiatCurrencyExchangeRate = event.getCurrencyRate();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void versionCheckStatus(VersionCheckStatusEvent event) {
|
||||
versionCheckService.cancel();
|
||||
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled()) {
|
||||
versionCheckService = createVersionCheckService();
|
||||
versionCheckService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
if(event.getWalletsMap().isEmpty()) {
|
||||
walletWindows.remove(event.getWindow());
|
||||
} else {
|
||||
walletWindows.put(event.getWindow(), event.getWalletsMap());
|
||||
}
|
||||
|
||||
List<Map.Entry<Wallet, Storage>> allWallets = walletWindows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(!Window.getWindows().isEmpty()) {
|
||||
List<File> walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList());
|
||||
Config.get().setRecentWalletFiles(walletFiles);
|
||||
}
|
||||
});
|
||||
|
||||
boolean usbWallet = false;
|
||||
for(Map.Entry<Wallet, Storage> entry : allWallets) {
|
||||
Wallet wallet = entry.getKey();
|
||||
Storage storage = entry.getValue();
|
||||
|
||||
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
usbWallet = true;
|
||||
|
||||
if(deviceEnumerateService == null) {
|
||||
deviceEnumerateService = createDeviceEnumerateService();
|
||||
}
|
||||
|
||||
if(deviceEnumerateService.getState() == Worker.State.CANCELLED) {
|
||||
deviceEnumerateService.reset();
|
||||
}
|
||||
|
||||
if(!deviceEnumerateService.isRunning()) {
|
||||
deviceEnumerateService.start();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!usbWallet && deviceEnumerateService != null && deviceEnumerateService.isRunning()) {
|
||||
deviceEnumerateService.cancel();
|
||||
EventManager.get().post(new UsbDeviceEvent(Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestConnect(RequestConnectEvent event) {
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||
onlineProperty.set(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
|
||||
public class Args {
|
||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||
public String dir;
|
||||
|
||||
@Parameter(names = { "--network", "-n" }, description = "Network to use")
|
||||
public Network network;
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.DescriptorArea;
|
||||
import com.sparrowwallet.sparrow.control.ScriptArea;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -39,4 +42,42 @@ public abstract class BaseController {
|
||||
protected String describeScriptChunk(ScriptChunk chunk) {
|
||||
return chunk.toString();
|
||||
}
|
||||
|
||||
protected void initializeDescriptorField(DescriptorArea descriptorArea) {
|
||||
Popup popup = new Popup();
|
||||
Label popupMsg = new Label();
|
||||
popupMsg.getStyleClass().add("tooltip");
|
||||
popup.getContent().add(popupMsg);
|
||||
|
||||
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
|
||||
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeKeystore(hoverKeystore));
|
||||
popup.show(descriptorArea, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
});
|
||||
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
||||
popup.hide();
|
||||
});
|
||||
}
|
||||
|
||||
protected String describeKeystore(Keystore keystore) {
|
||||
if(keystore.isValid()) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[");
|
||||
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
builder.append("/");
|
||||
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
|
||||
builder.append("]");
|
||||
builder.append(keystore.getExtendedPublicKey().toString());
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
return "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
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.io.Storage;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import javafx.application.Application;
|
||||
@@ -12,24 +17,39 @@ 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.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
private static final Logger log = LoggerFactory.getLogger(MainApp.class);
|
||||
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "0.9.9";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
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(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
|
||||
boolean createNewWallet = false;
|
||||
Mode mode = Config.get().getMode();
|
||||
@@ -48,23 +68,8 @@ public class MainApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml"));
|
||||
Parent root = transactionLoader.load();
|
||||
AppController appController = transactionLoader.getController();
|
||||
appController.setApplication(this);
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm());
|
||||
|
||||
stage.setTitle("Sparrow");
|
||||
stage.setMinWidth(650);
|
||||
stage.setMinHeight(700);
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
|
||||
|
||||
appController.initializeView();
|
||||
|
||||
stage.show();
|
||||
AppServices.initialize(this);
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
appController.newWallet(null);
|
||||
@@ -72,12 +77,21 @@ public class MainApp extends Application {
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
for(File walletFile : recentWalletFiles) {
|
||||
//Re-sort 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));
|
||||
appController.openWalletFile(walletFile, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,7 +99,42 @@ public class MainApp extends Application {
|
||||
mainStage.close();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase()).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
log.info("Using configured Sparrow home folder of " + args.dir);
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
Network.set(args.network);
|
||||
} else {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase()));
|
||||
} catch(Exception e) {
|
||||
log.warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
log.info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class TabData {
|
||||
private TabType type;
|
||||
private File file;
|
||||
private String text;
|
||||
private final TabType type;
|
||||
|
||||
public TabData(TabType type) {
|
||||
this.type = type;
|
||||
@@ -15,22 +11,6 @@ public class TabData {
|
||||
return type;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public enum TabType {
|
||||
WALLET, TRANSACTION
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
public enum Theme {
|
||||
LIGHT, DARK
|
||||
}
|
||||
@@ -4,14 +4,22 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class TransactionTabData extends TabData {
|
||||
private final File file;
|
||||
private final TransactionData transactionData;
|
||||
|
||||
public TransactionTabData(TabType type, TransactionData transactionData) {
|
||||
public TransactionTabData(TabType type, File file, TransactionData transactionData) {
|
||||
super(type);
|
||||
this.file = file;
|
||||
this.transactionData = transactionData;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public TransactionData getTransactionData() {
|
||||
return transactionData;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
Address address = utxoEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(getTooltipText(utxoEntry));
|
||||
setTooltip(tooltip);
|
||||
@@ -58,7 +58,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry) {
|
||||
return utxoEntry.getNode().getDerivationPath() + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
|
||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
|
||||
@@ -10,10 +11,10 @@ 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;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
public class AddressTreeTable extends CoinTreeTable {
|
||||
public void initialize(NodeEntry rootEntry) {
|
||||
@@ -33,7 +34,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(AppServices.getMonospaceFont(), address, 0.0));
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
@@ -60,7 +61,10 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
|
||||
Integer highestUsedIndex = rootEntry.getNode().getHighestUsedIndex();
|
||||
if(highestUsedIndex != null) {
|
||||
scrollTo(highestUsedIndex);
|
||||
OptionalInt tableIndex = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().getIndex() == highestUsedIndex + 1).mapToInt(childEntry -> rootEntry.getChildren().indexOf(childEntry)).findFirst();
|
||||
if(tableIndex.isPresent() && tableIndex.getAsInt() > 5) {
|
||||
scrollTo(tableIndex.getAsInt());
|
||||
}
|
||||
}
|
||||
|
||||
setOnMouseClicked(mouseEvent -> {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -81,9 +82,9 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
|
||||
for(int i = 0; i < balanceSeries.getData().size(); i++) {
|
||||
XYChart.Data<Number, Number> data = balanceSeries.getData().get(i);
|
||||
Node symbol = lookup(".chart-line-symbol.data" + i);
|
||||
if(symbol != null) {
|
||||
if(data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
|
||||
if(transactionEntry.getBlockTransaction().getDate() != null && data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime()) && data.getExtraValue() != null) {
|
||||
Node symbol = lookup(".chart-line-symbol.data" + i);
|
||||
if(symbol != null) {
|
||||
symbol.getStyleClass().add("selected");
|
||||
selectedEntry = transactionEntry;
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ import javafx.scene.chart.XYChart;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
public class FeeRatesChart extends LineChart<String, Number> {
|
||||
public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||
private XYChart.Series<String, Number> feeRateSeries;
|
||||
private Integer selectedTargetBlocks;
|
||||
|
||||
public FeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
public BlockTargetFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
@@ -33,4 +36,25 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
|
||||
if(getRoot() != null) {
|
||||
Entry entry = getRoot().getValue();
|
||||
if(entry != null && event.getWallet() != null && entry.getWallet() == event.getWallet()) {
|
||||
Platform.runLater(() -> {
|
||||
if(event.getErrorMessage() != null) {
|
||||
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
|
||||
} else if(event.isLoading()) {
|
||||
if(event.getStatusMessage() != null) {
|
||||
setPlaceholder(new Label(event.getStatusMessage() + "..."));
|
||||
} else {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
} else {
|
||||
setPlaceholder(new Label("No transactions"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
|
||||
|
||||
public class DescriptorArea extends CodeArea {
|
||||
private Wallet wallet;
|
||||
|
||||
public void setWallet(Wallet wallet) {
|
||||
clear();
|
||||
this.wallet = wallet;
|
||||
|
||||
DescriptorContextMenu contextMenu = new DescriptorContextMenu(wallet, this);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
PolicyType policyType = wallet.getPolicyType();
|
||||
ScriptType scriptType = wallet.getScriptType();
|
||||
List<Keystore> keystores = wallet.getKeystores();
|
||||
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
|
||||
if(SINGLE.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(MULTI.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
append(MULTISIG.getDescriptor(), "descriptor-text");
|
||||
append(Integer.toString(threshold), "descriptor-text");
|
||||
|
||||
for(Keystore keystore : keystores) {
|
||||
append(",", "descriptor-text");
|
||||
replace(getLength(), getLength(), keystore.getScriptName(), List.of(keystore.isValid() ? "descriptor-text" : "descriptor-error", keystore.getScriptName()));
|
||||
}
|
||||
|
||||
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
super.clear();
|
||||
this.wallet = null;
|
||||
setDisable(false);
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
private static class DescriptorContextMenu extends ContextMenu {
|
||||
public DescriptorContextMenu(Wallet wallet, DescriptorArea descriptorArea) {
|
||||
MenuItem copyvalue = new MenuItem("Copy Value");
|
||||
copyvalue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(descriptorArea.getText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyvalue);
|
||||
|
||||
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
|
||||
copyOutputDescriptor.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyOutputDescriptor);
|
||||
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ 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;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
@@ -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());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
@@ -64,7 +67,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppController.getDevices();
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanBox.setVisible(true);
|
||||
} else {
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
@@ -10,11 +11,13 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -29,14 +32,19 @@ import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||
|
||||
private final DeviceOperation deviceOperation;
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final String message;
|
||||
private final Device device;
|
||||
|
||||
private CustomPasswordField pinField;
|
||||
@@ -46,6 +54,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private SplitMenuButton importButton;
|
||||
private Button signButton;
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
|
||||
@@ -55,6 +64,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
|
||||
setDefaultStatus();
|
||||
@@ -63,7 +73,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
createSetPassphraseButton();
|
||||
createImportButton();
|
||||
|
||||
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
if(device.getError() != null) {
|
||||
setError("Error", device.getError());
|
||||
Platform.runLater(() -> {
|
||||
setExpanded(true);
|
||||
});
|
||||
} else if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
@@ -80,6 +95,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = null;
|
||||
this.psbt = psbt;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
|
||||
setDefaultStatus();
|
||||
@@ -105,6 +121,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
|
||||
setDefaultStatus();
|
||||
@@ -124,6 +141,32 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = message;
|
||||
this.device = device;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
createSetPassphraseButton();
|
||||
createSignMessageButton();
|
||||
|
||||
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
}
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createUnlockButton();
|
||||
@@ -166,7 +209,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation());
|
||||
});
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
for(int i = 0; i < accounts.length; i++) {
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
@@ -208,8 +252,24 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void createSignMessageButton() {
|
||||
signMessageButton = new Button("Sign Message");
|
||||
signMessageButton.setDefaultButton(true);
|
||||
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
signMessageButton.setOnAction(event -> {
|
||||
signMessageButton.setDisable(true);
|
||||
signMessage();
|
||||
});
|
||||
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
|
||||
signMessageButton.setVisible(false);
|
||||
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
|
||||
signMessageButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().equals(WalletModel.TREZOR_1)) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
}
|
||||
}
|
||||
@@ -273,6 +333,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 +404,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);
|
||||
@@ -415,6 +476,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
@@ -436,6 +498,20 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
displayAddressService.start();
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
signMessageService.setOnFailed(failedEvent -> {
|
||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||
signMessageButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing message...");
|
||||
signMessageService.start();
|
||||
}
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
importButton.setVisible(true);
|
||||
@@ -448,6 +524,9 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
|
||||
displayAddressButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
|
||||
signMessageButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,6 +567,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS;
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ 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 -> {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceSignMessageDialog extends DeviceDialog<String> {
|
||||
private final Wallet wallet;
|
||||
private final String message;
|
||||
private final KeyDerivation keyDerivation;
|
||||
|
||||
public DeviceSignMessageDialog(List<String> operationFingerprints, Wallet wallet, String message, KeyDerivation keyDerivation) {
|
||||
super(operationFingerprints);
|
||||
this.wallet = wallet;
|
||||
this.message = message;
|
||||
this.keyDerivation = keyDerivation;
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, message, keyDerivation, device);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void messageSigned(MessageSignedEvent event) {
|
||||
if(wallet == event.getWallet()) {
|
||||
setResult(event.getSignature());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import tornadofx.control.Form;
|
||||
|
||||
public class DynamicForm extends Form {
|
||||
private DynamicUpdate dynamicUpdate;
|
||||
|
||||
public DynamicForm() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void setDynamicUpdate(DynamicUpdate dynamicUpdate) {
|
||||
this.dynamicUpdate = dynamicUpdate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
if(dynamicUpdate != null) {
|
||||
dynamicUpdate.update();
|
||||
}
|
||||
|
||||
super.layoutChildren();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
public interface DynamicUpdate {
|
||||
void update();
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -18,7 +21,7 @@ import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
@@ -43,31 +46,56 @@ 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));
|
||||
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
|
||||
setText("Unconfirmed");
|
||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||
} else {
|
||||
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
|
||||
setText(date);
|
||||
setContextMenu(new TransactionContextMenu(date, transactionEntry.getBlockTransaction()));
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
setTooltip(tooltip);
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button viewTransactionButton = new Button("");
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
|
||||
});
|
||||
setGraphic(viewTransactionButton);
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||
increaseFeeGlyph.setFontSize(12);
|
||||
increaseFeeButton.setGraphic(increaseFeeGlyph);
|
||||
increaseFeeButton.setOnAction(event -> {
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
actionBox.getChildren().add(increaseFeeButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath());
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button receiveButton = new Button("");
|
||||
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
|
||||
receiveGlyph.setFontSize(12);
|
||||
@@ -76,11 +104,27 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
setGraphic(receiveButton);
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
|
||||
(nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
||||
Button signMessageButton = new Button("");
|
||||
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||
signMessageGlyph.setFontSize(12);
|
||||
signMessageButton.setGraphic(signMessageGlyph);
|
||||
signMessageButton.setOnAction(event -> {
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
actionBox.getChildren().add(signMessageButton);
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry));
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||
setTooltip(tooltip);
|
||||
@@ -91,30 +135,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry));
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
|
||||
});
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent()) {
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
Button spendUtxoButton = new Button("");
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.setFontSize(12);
|
||||
spendUtxoButton.setGraphic(sendGlyph);
|
||||
spendUtxoButton.setOnAction(event -> {
|
||||
List<HashIndexEntry> utxoEntries = getTreeTableView().getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.isSpent())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(!utxoEntries.contains(hashIndexEntry)) {
|
||||
utxoEntries = List.of(hashIndexEntry);
|
||||
}
|
||||
|
||||
final List<HashIndexEntry> spendingUtxoEntries = utxoEntries;
|
||||
EventManager.get().post(new SendActionEvent(utxoEntries));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries)));
|
||||
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
|
||||
});
|
||||
actionBox.getChildren().add(spendUtxoButton);
|
||||
}
|
||||
@@ -124,6 +155,112 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
int vSize = tx.getVirtualSize();
|
||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||
//If there is insufficent change output, include another random UTXO so the fee can be increased
|
||||
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
||||
utxos.add(utxo);
|
||||
changeTotal += utxo.getValue();
|
||||
vSize += inputSize;
|
||||
}
|
||||
|
||||
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||
externalOutputs.removeAll(ourOutputs);
|
||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||
try {
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
if(AppServices.getTargetBlockFeeRates().isEmpty()) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
return AppServices.getTargetBlockFeeRates().values().iterator().next();
|
||||
}
|
||||
|
||||
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(!utxoEntries.contains(hashIndexEntry)) {
|
||||
utxoEntries = List.of(hashIndexEntry);
|
||||
}
|
||||
|
||||
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||
EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
}
|
||||
|
||||
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(null);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
}
|
||||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(blockTransaction.getHashAsString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copyTxid);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee");
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
@@ -155,7 +292,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor) {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -181,11 +318,22 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
|
||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
hide();
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HashIndexEntryContextMenu extends ContextMenu {
|
||||
public HashIndexEntryContextMenu(HashIndexEntry hashIndexEntry) {
|
||||
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
|
||||
MenuItem copyHashIndex = new MenuItem(label);
|
||||
copyHashIndex.setOnAction(AE -> {
|
||||
@@ -194,17 +342,46 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
content.putString(hashIndexEntry.getHashIndex().toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copyHashIndex);
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
MenuItem sendSelected = new MenuItem("Send Selected");
|
||||
sendSelected.setOnAction(AE -> {
|
||||
hide();
|
||||
sendSelectedUtxos(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(sendSelected);
|
||||
}
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
if(hashIndexEntry.getHashIndex().getStatus() == null || hashIndexEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||
MenuItem freezeUtxo = new MenuItem("Freeze UTXO");
|
||||
freezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
freezeUtxo(hashIndexEntry);
|
||||
});
|
||||
getItems().add(freezeUtxo);
|
||||
} else {
|
||||
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
|
||||
unfreezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
unfreezeUtxo(hashIndexEntry);
|
||||
});
|
||||
getItems().add(unfreezeUtxo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +399,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);
|
||||
|
||||
@@ -2,6 +2,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.FileImport;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -9,41 +13,75 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileImportPane.class);
|
||||
|
||||
private final FileImport importer;
|
||||
private Button importButton;
|
||||
protected ButtonBase importButton;
|
||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||
private final boolean scannable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl) {
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||
super(title, description, content, imageUrl);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
return importButton;
|
||||
if(scannable) {
|
||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
scanButton.setGraphic(cameraGlyph);
|
||||
scanButton.setOnAction(event -> {
|
||||
scanButton.setSelected(false);
|
||||
importQR();
|
||||
});
|
||||
|
||||
ToggleButton fileButton = new ToggleButton("Import File...");
|
||||
fileButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
fileButton.setOnAction(event -> {
|
||||
fileButton.setSelected(false);
|
||||
importFile();
|
||||
});
|
||||
importButton = fileButton;
|
||||
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
return importButton;
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
@@ -52,8 +90,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", "*.*"),
|
||||
new FileChooser.ExtensionFilter("JSON", "*.json")
|
||||
new FileChooser.ExtensionFilter("All Files", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("JSON", "*.json"),
|
||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||
);
|
||||
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
@@ -93,6 +132,53 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void importQR() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.wallets != null) {
|
||||
wallets = result.wallets;
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
try {
|
||||
importFile(importer.getName(), new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8)), null);
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing QR", e);
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) {
|
||||
errorMessage = "QR contents were not in JSON format";
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
}
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error importing QR", result.exception);
|
||||
setError("Import Error", result.exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
|
||||
if(wallets != null) {
|
||||
for(Wallet wallet : wallets) {
|
||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
return wallet.getKeystores().get(0);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImportException("Script type " + scriptType + " is not supported");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
||||
|
||||
private Node getPasswordEntry(File file) {
|
||||
@@ -102,6 +188,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||
|
||||
Button importEncryptedButton = new Button("Import");
|
||||
importEncryptedButton.setDefaultButton(true);
|
||||
importEncryptedButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
|
||||
@@ -14,13 +14,17 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
private final KeystoreFileImport importer;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Keystore file import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
Keystore keystore = getScannedKeystore(wallet.getScriptType());
|
||||
if(keystore == null) {
|
||||
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
private final WalletImport importer;
|
||||
|
||||
public FileWalletImportPane(WalletImport importer) {
|
||||
super(importer, importer.getName(), "Wallet file import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public class HelpLabel extends Label {
|
||||
private StringProperty helpText;
|
||||
|
||||
public final void setHelpText(String value) {
|
||||
helpTextProperty().setValue(value);
|
||||
helpTextProperty().setValue(value.replace("\\n", "\n"));
|
||||
}
|
||||
|
||||
public final String getHelpText() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.scene.text.Font;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
|
||||
public class IdLabel extends CopyableLabel {
|
||||
public IdLabel() {
|
||||
@@ -9,6 +9,6 @@ public class IdLabel extends CopyableLabel {
|
||||
|
||||
public IdLabel(String text) {
|
||||
super(text);
|
||||
setFont(Font.font("Courier"));
|
||||
setFont(AppServices.getMonospaceFont());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@@ -25,7 +25,8 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
|
||||
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
setCreateSymbols(false);
|
||||
setCursor(Cursor.CROSSHAIR);
|
||||
setVerticalGridLinesVisible(false);
|
||||
tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.ZERO);
|
||||
tooltip.setHideDelay(Duration.ZERO);
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
Platform.runLater(() -> {
|
||||
Node node = this.lookup(".plot-content");
|
||||
Tooltip.install(node, tooltip);
|
||||
});
|
||||
}
|
||||
|
||||
public void update(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||
getData().clear();
|
||||
if(tooltip.isShowing()) {
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
Map<Date, Set<MempoolRateSize>> periodRateSizes = getPeriodRateSizes(mempoolRateSizes);
|
||||
Map<Date, String> categories = getCategories(periodRateSizes);
|
||||
|
||||
CategoryAxis categoryAxis = (CategoryAxis)getXAxis();
|
||||
categoryAxis.setTickMarkVisible(false);
|
||||
categoryAxis.setTickLabelGap(10);
|
||||
categoryAxis.setAutoRanging(false);
|
||||
categoryAxis.setCategories(FXCollections.observableArrayList(categories.values()));
|
||||
categoryAxis.invalidateRange(new ArrayList<>(categories.values()));
|
||||
categoryAxis.setGapStartAndEnd(false);
|
||||
categoryAxis.setTickLabelRotation(0);
|
||||
|
||||
NumberAxis numberAxis = (NumberAxis)getYAxis();
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
this.setOnMouseMoved(mouseEvent -> {
|
||||
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
|
||||
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
|
||||
if(category != null) {
|
||||
Optional<String> time = categories.entrySet().stream().filter(entry -> entry.getValue().equals(category)).map(entry -> dateFormatter.format(entry.getKey())).findFirst();
|
||||
time.ifPresent(s -> tooltip.setGraphic(new ChartTooltip(category, s, getData())));
|
||||
}
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
||||
long totalVSize = 0;
|
||||
for(MempoolRateSize rateSize : rateSizes) {
|
||||
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
|
||||
totalVSize += rateSize.getVSize();
|
||||
}
|
||||
}
|
||||
|
||||
series.getData().add(new XYChart.Data<>(categories.get(date), totalVSize));
|
||||
seriesTotalVSize += totalVSize;
|
||||
}
|
||||
|
||||
if(seriesTotalVSize > 0) {
|
||||
getData().add(series);
|
||||
}
|
||||
|
||||
previousFeeRate = feeRate;
|
||||
}
|
||||
|
||||
if(categories.keySet().iterator().hasNext()) {
|
||||
String time = categories.values().iterator().next();
|
||||
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
|
||||
numberAxis.setTickLabelsVisible(true);
|
||||
numberAxis.setOpacity(1);
|
||||
} else {
|
||||
numberAxis.setTickLabelsVisible(false);
|
||||
numberAxis.setOpacity(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||
if(mempoolRateSizes.size() == 1) {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
|
||||
LocalDateTime period = LocalDateTime.now().minusHours(MAX_PERIOD_HOURS);
|
||||
return mempoolRateSizes.entrySet().stream().filter(entry -> {
|
||||
LocalDateTime dateTime = entry.getKey().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
return dateTime.isAfter(period);
|
||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate dates"); },
|
||||
TreeMap::new));
|
||||
}
|
||||
|
||||
private Map<Date, String> getCategories(Map<Date, Set<MempoolRateSize>> mempoolHistogram) {
|
||||
Map<Date, String> categories = new LinkedHashMap<>();
|
||||
|
||||
String invisible = "" + '\ufeff';
|
||||
for(Iterator<Date> iter = mempoolHistogram.keySet().iterator(); iter.hasNext(); ) {
|
||||
Date date = iter.next();
|
||||
String label = dateFormatter.format(date);
|
||||
if(!categories.isEmpty() && iter.hasNext()) {
|
||||
label = invisible;
|
||||
invisible += '\ufeff';
|
||||
}
|
||||
|
||||
categories.put(date, label);
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static class ChartTooltip extends VBox {
|
||||
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
|
||||
Label title = new Label("At " + time);
|
||||
HBox titleBox = new HBox(title);
|
||||
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
|
||||
getChildren().add(titleBox);
|
||||
|
||||
for(int i = seriesList.size() - 1; i >= 0; i--) {
|
||||
Series<String, Number> series = seriesList.get(i);
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
if(data.getXValue().equals(category)) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
if(mvb >= 0.01) {
|
||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||
}
|
||||
label.setGraphic(circle);
|
||||
getChildren().add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.security.SignatureException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
private final Wallet wallet;
|
||||
private WalletNode walletNode;
|
||||
|
||||
/**
|
||||
* Verification only constructor
|
||||
*/
|
||||
public MessageSignDialog() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and verify with user entered address
|
||||
*
|
||||
* @param wallet Wallet to sign with
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet) {
|
||||
this(wallet, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and verify with preset address
|
||||
*
|
||||
* @param wallet Wallet to sign with
|
||||
* @param walletNode Wallet node to derive address from
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
|
||||
if(wallet != null) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
|
||||
}
|
||||
}
|
||||
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if (!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
address = new TextField();
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
address.setText(wallet.getAddress(walletNode).toString());
|
||||
}
|
||||
|
||||
Field messageField = new Field();
|
||||
messageField.setText("Message:");
|
||||
message = new TextArea();
|
||||
message.setWrapText(true);
|
||||
message.setPrefRowCount(10);
|
||||
message.setStyle("-fx-pref-height: 180px");
|
||||
messageField.getInputs().add(message);
|
||||
|
||||
Field signatureField = new Field();
|
||||
signatureField.setText("Signature:");
|
||||
signature = new TextArea();
|
||||
signature.getStyleClass().add("id");
|
||||
signature.setPrefRowCount(2);
|
||||
signature.setStyle("-fx-pref-height: 60px");
|
||||
signature.setWrapText(true);
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
fieldset.getChildren().addAll(addressField, messageField, signatureField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(wallet == null);
|
||||
signButton.setOnAction(event -> {
|
||||
signMessage();
|
||||
});
|
||||
|
||||
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
|
||||
verifyButton.setDefaultButton(false);
|
||||
verifyButton.setOnAction(event -> {
|
||||
verifyMessage();
|
||||
});
|
||||
|
||||
boolean validAddress = isValidAddress();
|
||||
signButton.setDisable(!validAddress || (wallet == null));
|
||||
verifyButton.setDisable(!validAddress);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = isValidAddress();
|
||||
signButton.setDisable(!valid || (wallet == null));
|
||||
verifyButton.setDisable(!valid);
|
||||
|
||||
if(valid && wallet != null) {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
if(ButtonBar.ButtonData.APPLY.equals(getResult())) {
|
||||
event.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setWalletNodeFromAddress(Wallet wallet, Address address) {
|
||||
walletNode = wallet.getWalletAddresses().get(address);
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(wallet.containsSeeds()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
signUnencryptedKeystore(wallet);
|
||||
}
|
||||
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
//Note we can expect a single keystore due to the check above
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
} catch(Exception e) {
|
||||
log.error("Could not sign message", e);
|
||||
AppServices.showErrorDialog("Could not sign message", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void signUsbKeystore(Wallet usbWallet) {
|
||||
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
if(optSignature.isPresent()) {
|
||||
signature.clear();
|
||||
signature.appendText(optSignature.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMessage() {
|
||||
try {
|
||||
//Find ECKey from message and signature
|
||||
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
|
||||
boolean verified = false;
|
||||
try {
|
||||
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText().trim(), signature.getText().trim(), false);
|
||||
verified = verifyMessage(signedMessageKey);
|
||||
} catch(SignatureException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
if(!verified) {
|
||||
try {
|
||||
ECKey electrumSignedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText(), true);
|
||||
verified = verifyMessage(electrumSignedMessageKey);
|
||||
} catch(SignatureException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(verified) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.setTitle("Verification Succeeded");
|
||||
alert.setHeaderText("Verification Succeeded");
|
||||
alert.setContentText("The signature verified against the message.");
|
||||
alert.showAndWait();
|
||||
} else {
|
||||
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
|
||||
}
|
||||
} catch(IllegalArgumentException e) {
|
||||
AppServices.showErrorDialog("Could not verify message", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
log.error("Could not verify message", e);
|
||||
AppServices.showErrorDialog("Could not verify message", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyMessage(ECKey signedMessageKey) throws InvalidAddressException, SignatureException {
|
||||
Address providedAddress = getAddress();
|
||||
ScriptType scriptType = providedAddress.getScriptType();
|
||||
if(scriptType == ScriptType.P2SH) {
|
||||
scriptType = ScriptType.P2SH_P2WPKH;
|
||||
}
|
||||
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
|
||||
}
|
||||
|
||||
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
|
||||
return providedAddress.equals(signedMessageAddress);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
Storage storage = event.getStorage(wallet);
|
||||
if(storage == null) {
|
||||
//Another window, ignore
|
||||
return;
|
||||
}
|
||||
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
signUnencryptedKeystore(decryptedWallet);
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
@@ -54,7 +55,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;
|
||||
|
||||
@@ -81,7 +82,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Set Words Length");
|
||||
enterMnemonicButton.setText("Set Words Size");
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
enterMnemonic(24);
|
||||
});
|
||||
@@ -107,7 +108,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
for(int i = 0; i < accounts.length; i++) {
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
@@ -188,7 +190,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
confirmButton = new Button("Confirm Written Backup");
|
||||
confirmButton = new Button("Re-enter Words...");
|
||||
confirmButton.setOnAction(event -> {
|
||||
confirmBackup();
|
||||
});
|
||||
@@ -269,7 +271,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void displayMnemonicCode() {
|
||||
setDescription("Write down word list to confirm backup");
|
||||
setDescription("Write down words before re-entering");
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
calculateButton.setVisible(false);
|
||||
|
||||
@@ -5,20 +5,22 @@ 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.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.ur.UR;
|
||||
import com.sparrowwallet.sparrow.ur.UREncoder;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -26,47 +28,89 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 100;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 400;
|
||||
|
||||
private final UR ur;
|
||||
private final UREncoder encoder;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
private AnimateQRService animateQRService;
|
||||
private String currentPart;
|
||||
|
||||
public QRDisplayDialog(byte[] data) {
|
||||
this(UR.fromBytes(data));
|
||||
private boolean useLegacyEncoding;
|
||||
private String[] legacyParts;
|
||||
private int legacyPartIndex;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), addLegacyEncodingOption);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur) {
|
||||
this(ur, false);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
|
||||
this.ur = ur;
|
||||
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
qrImageView = new ImageView();
|
||||
stackPane.getChildren().add(qrImageView);
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
nextPart();
|
||||
if(encoder.isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
AnimateQRService animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(100));
|
||||
animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
|
||||
animateQRService.start();
|
||||
setOnCloseRequest(event -> {
|
||||
animateQRService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
|
||||
if(addLegacyEncodingOption) {
|
||||
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(550);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(String data) {
|
||||
this.ur = null;
|
||||
this.encoder = null;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
qrImageView = new ImageView();
|
||||
stackPane.getChildren().add(qrImageView);
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.setPrefWidth(500);
|
||||
@@ -76,8 +120,16 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
String fragment = encoder.nextPart();
|
||||
currentPart = fragment.toUpperCase();
|
||||
if(!useLegacyEncoding) {
|
||||
String fragment = encoder.nextPart();
|
||||
currentPart = fragment.toUpperCase();
|
||||
} else {
|
||||
currentPart = legacyParts[legacyPartIndex];
|
||||
legacyPartIndex++;
|
||||
if(legacyPartIndex > legacyParts.length - 1) {
|
||||
legacyPartIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Image getQrCode(String fragment) {
|
||||
@@ -97,6 +149,44 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setUseLegacyEncoding(boolean useLegacyEncoding) {
|
||||
if(useLegacyEncoding) {
|
||||
try {
|
||||
//Force to be bytes type for legacy encoding
|
||||
LegacyUREncoder legacyEncoder = new LegacyUREncoder(new UR(RegistryType.BYTES.toString(), ur.getCborBytes()));
|
||||
this.legacyParts = legacyEncoder.encode();
|
||||
this.useLegacyEncoding = true;
|
||||
|
||||
if(legacyParts.length == 1) {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
nextPart();
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
}
|
||||
} catch(UR.InvalidTypeException e) {
|
||||
//Can't happen
|
||||
}
|
||||
} else {
|
||||
this.useLegacyEncoding = false;
|
||||
|
||||
if(encoder.isSinglePart()) {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AnimateQRService extends ScheduledService<Boolean> {
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
@@ -111,4 +201,44 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class QRDisplayDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton legacy = new ToggleButton(buttonType.getText());
|
||||
legacy.setGraphicTextGap(5);
|
||||
setLegacyGraphic(legacy, false);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(legacy, buttonData);
|
||||
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseLegacyEncoding(newValue);
|
||||
setLegacyGraphic(legacy, newValue);
|
||||
});
|
||||
|
||||
return legacy;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
|
||||
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
|
||||
if(useLegacyEncoding) {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, "success"));
|
||||
} else {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(12);
|
||||
if(styleClass != null) {
|
||||
glyph.getStyleClass().add(styleClass);
|
||||
}
|
||||
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.sparrow.ur.ResultType;
|
||||
import com.sparrowwallet.sparrow.ur.UR;
|
||||
import com.sparrowwallet.sparrow.ur.URDecoder;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.LegacyURDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.*;
|
||||
import com.sparrowwallet.hummingbird.ResultType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -19,26 +32,46 @@ import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
|
||||
|
||||
private final URDecoder decoder;
|
||||
private final LegacyURDecoder legacyDecoder;
|
||||
private final WebcamService webcamService;
|
||||
private List<String> parts;
|
||||
|
||||
private boolean isUr;
|
||||
private QRScanDialog.Result result;
|
||||
|
||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
|
||||
this.webcamService = new WebcamService(WebcamResolution.VGA);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(webcamView.getView());
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
webcamService.resultProperty().addListener(new QRResultListener());
|
||||
webcamService.setOnFailed(failedEvent -> {
|
||||
@@ -66,43 +99,79 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
//Try text first
|
||||
String qrtext = qrResult.getText();
|
||||
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
|
||||
|
||||
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
isUr = true;
|
||||
decoder.receivePart(qrtext);
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
if(urResult.type == ResultType.SUCCESS) {
|
||||
if(urResult.ur.getType().equals(UR.BYTES_TYPE)) {
|
||||
try {
|
||||
PSBT psbt = new PSBT(urResult.ur.toBytes());
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext.toLowerCase());
|
||||
|
||||
try {
|
||||
Transaction transaction = new Transaction(urResult.ur.toBytes());
|
||||
result = new Result(transaction);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as tx
|
||||
}
|
||||
|
||||
result = new Result("Parsed UR of type " + urResult.ur.getType() + " was not a PSBT or transaction");
|
||||
} else {
|
||||
result = new Result("Cannot parse UR type of " + urResult.ur.getType());
|
||||
if(legacyDecoder.isComplete()) {
|
||||
try {
|
||||
UR ur = legacyDecoder.decode();
|
||||
result = extractResultFromUR(ur);
|
||||
} catch(Exception e) {
|
||||
result = new Result(new URException(e.getMessage()));
|
||||
}
|
||||
} else {
|
||||
result = new Result(urResult.error);
|
||||
}
|
||||
} else {
|
||||
decoder.receivePart(qrtext);
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
if(urResult.type == ResultType.SUCCESS) {
|
||||
result = extractResultFromUR(urResult.ur);
|
||||
} else {
|
||||
result = new Result(new URException(urResult.error));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(partMatcher.matches()) {
|
||||
int m = Integer.parseInt(partMatcher.group(1));
|
||||
int n = Integer.parseInt(partMatcher.group(2));
|
||||
String payload = partMatcher.group(3);
|
||||
|
||||
if(parts == null) {
|
||||
parts = new ArrayList<>(n);
|
||||
IntStream.range(0, n).forEach(i -> parts.add(null));
|
||||
}
|
||||
parts.set(m - 1, payload);
|
||||
|
||||
if(parts.stream().filter(Objects::nonNull).count() == n) {
|
||||
String complete = String.join("", parts);
|
||||
try {
|
||||
PSBT psbt = PSBT.fromString(complete);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
|
||||
try {
|
||||
Transaction transaction = new Transaction(Utils.hexToBytes(complete));
|
||||
result = new Result(transaction);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as tx
|
||||
}
|
||||
|
||||
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
|
||||
}
|
||||
} else {
|
||||
PSBT psbt;
|
||||
Transaction transaction;
|
||||
BitcoinURI bitcoinURI;
|
||||
Address address;
|
||||
ExtendedKey extendedKey;
|
||||
try {
|
||||
extendedKey = ExtendedKey.fromDescriptor(qrtext);
|
||||
result = new Result(extendedKey);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not a valid xpub
|
||||
}
|
||||
|
||||
try {
|
||||
bitcoinURI = new BitcoinURI(qrtext);
|
||||
result = new Result(bitcoinURI);
|
||||
@@ -153,7 +222,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
//Try Base43 used by Electrum
|
||||
try {
|
||||
psbt = new PSBT(Base43.decode(qrResult.getText()));
|
||||
psbt = new PSBT(Base43.decode(qrtext));
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
@@ -161,30 +230,219 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
|
||||
try {
|
||||
transaction = new Transaction(Base43.decode(qrResult.getText()));
|
||||
transaction = new Transaction(Base43.decode(qrtext));
|
||||
result = new Result(transaction);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as base43 decoded bytes
|
||||
}
|
||||
|
||||
result = new Result("Cannot parse QR code into a PSBT, transaction or address");
|
||||
result = new Result(qrtext);
|
||||
}
|
||||
}
|
||||
|
||||
private Result extractResultFromUR(UR ur) {
|
||||
try {
|
||||
RegistryType urRegistryType = ur.getRegistryType();
|
||||
|
||||
if(urRegistryType.equals(RegistryType.BYTES)) {
|
||||
byte[] urBytes = (byte[])ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(urBytes);
|
||||
return new Result(psbt);
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
|
||||
try {
|
||||
Transaction transaction = new Transaction(urBytes);
|
||||
return new Result(transaction);
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as tx
|
||||
}
|
||||
|
||||
try {
|
||||
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
|
||||
ByteBuffer buf = ByteBuffer.wrap(urBytes);
|
||||
CharBuffer charBuffer = decoder.decode(buf);
|
||||
return new Result(charBuffer.toString());
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as utf-8
|
||||
}
|
||||
|
||||
result = new Result(new URException("Parsed UR of type " + urRegistryType + " was not a PSBT, transaction or UTF-8 text"));
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
|
||||
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
|
||||
return new Result(psbt);
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
|
||||
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
|
||||
}
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_ADDRESS)) {
|
||||
CryptoAddress cryptoAddress = (CryptoAddress)ur.decodeFromRegistry();
|
||||
Address address = getAddress(cryptoAddress);
|
||||
if(address != null) {
|
||||
return new Result(BitcoinURI.fromAddress(address));
|
||||
} else {
|
||||
return new Result(new URException("Unknown " + urRegistryType + " type of " + cryptoAddress.getType()));
|
||||
}
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
|
||||
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
|
||||
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
|
||||
return new Result(extendedKey);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
|
||||
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
return new Result(outputDescriptor);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_ACCOUNT)) {
|
||||
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
|
||||
List<Wallet> wallets = getWallets(cryptoAccount);
|
||||
return new Result(wallets);
|
||||
} else {
|
||||
log.error("Unsupported UR type " + urRegistryType);
|
||||
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
|
||||
}
|
||||
} catch(IllegalArgumentException e) {
|
||||
log.error("Unknown UR type of " + ur.getType(), e);
|
||||
return new Result(new URException("Unknown UR type of " + ur.getType(), e));
|
||||
} catch(UR.InvalidCBORException e) {
|
||||
log.error("Invalid CBOR in UR", e);
|
||||
return new Result(new URException("Invalid CBOR in UR", e));
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing UR CBOR", e);
|
||||
return new Result(new URException("Error parsing UR CBOR", e));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Address getAddress(CryptoAddress cryptoAddress) {
|
||||
Address address = null;
|
||||
if(cryptoAddress.getType() == null || cryptoAddress.getType() == CryptoAddress.Type.P2PKH) {
|
||||
address = new P2PKHAddress(cryptoAddress.getData());
|
||||
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2SH) {
|
||||
address = new P2SHAddress(cryptoAddress.getData());
|
||||
} else if(cryptoAddress.getType() == CryptoAddress.Type.P2WPKH) {
|
||||
address = new P2WPKHAddress(cryptoAddress.getData());
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
private ExtendedKey getExtendedKey(CryptoHDKey cryptoHDKey) {
|
||||
if(cryptoHDKey.isPrivateKey()) {
|
||||
DeterministicKey prvKey = HDKeyDerivation.createMasterPrivKeyFromBytes(Arrays.copyOfRange(cryptoHDKey.getKey(), 1, 33), cryptoHDKey.getChainCode(), List.of(ChildNumber.ZERO));
|
||||
return new ExtendedKey(prvKey, new byte[4], ChildNumber.ZERO);
|
||||
} else {
|
||||
ChildNumber lastChild = ChildNumber.ZERO;
|
||||
int depth = 1;
|
||||
byte[] parentFingerprint = new byte[4];
|
||||
if(cryptoHDKey.getOrigin() != null) {
|
||||
if(!cryptoHDKey.getOrigin().getComponents().isEmpty()) {
|
||||
PathComponent lastComponent = cryptoHDKey.getOrigin().getComponents().get(cryptoHDKey.getOrigin().getComponents().size() - 1);
|
||||
lastChild = new ChildNumber(lastComponent.getIndex(), lastComponent.isHardened());
|
||||
depth = cryptoHDKey.getOrigin().getComponents().size();
|
||||
}
|
||||
if(cryptoHDKey.getParentFingerprint() != null) {
|
||||
parentFingerprint = cryptoHDKey.getParentFingerprint();
|
||||
}
|
||||
}
|
||||
DeterministicKey pubKey = new DeterministicKey(List.of(lastChild), cryptoHDKey.getChainCode(), cryptoHDKey.getKey(), depth, parentFingerprint);
|
||||
return new ExtendedKey(pubKey, parentFingerprint, lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
|
||||
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
|
||||
|
||||
if(cryptoOutput.getMultiKey() != null) {
|
||||
MultiKey multiKey = cryptoOutput.getMultiKey();
|
||||
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||
for(CryptoHDKey cryptoHDKey : multiKey.getHdKeys()) {
|
||||
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
|
||||
KeyDerivation keyDerivation = getKeyDerivation(cryptoHDKey.getOrigin());
|
||||
extendedPublicKeys.put(extendedKey, keyDerivation);
|
||||
}
|
||||
return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys);
|
||||
} else if(cryptoOutput.getEcKey() != null) {
|
||||
throw new IllegalArgumentException("EC keys are currently unsupported");
|
||||
} else if(cryptoOutput.getHdKey() != null) {
|
||||
ExtendedKey extendedKey = getExtendedKey(cryptoOutput.getHdKey());
|
||||
KeyDerivation keyDerivation = getKeyDerivation(cryptoOutput.getHdKey().getOrigin());
|
||||
return new OutputDescriptor(scriptType, extendedKey, keyDerivation);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("CryptoOutput did not contain sufficient information");
|
||||
}
|
||||
|
||||
private List<Wallet> getWallets(CryptoAccount cryptoAccount) {
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
|
||||
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
|
||||
Wallet wallet = new Wallet();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
if(outputDescriptor.isMultisig()) {
|
||||
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
|
||||
}
|
||||
|
||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
return wallets;
|
||||
}
|
||||
|
||||
private ScriptType getScriptType(List<ScriptExpression> expressions) {
|
||||
if(List.of(ScriptExpression.PUBLIC_KEY_HASH).equals(expressions)) {
|
||||
return ScriptType.P2PKH;
|
||||
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||
return ScriptType.P2SH_P2WPKH;
|
||||
} else if(List.of(ScriptExpression.WITNESS_PUBLIC_KEY_HASH).equals(expressions)) {
|
||||
return ScriptType.P2WPKH;
|
||||
} else if(List.of(ScriptExpression.SCRIPT_HASH).equals(expressions)) {
|
||||
return ScriptType.P2SH;
|
||||
} else if(List.of(ScriptExpression.SCRIPT_HASH, ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||
return ScriptType.P2SH_P2WSH;
|
||||
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||
return ScriptType.P2WSH;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown script of " + expressions);
|
||||
}
|
||||
|
||||
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
|
||||
if(cryptoKeypath != null) {
|
||||
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
public final Transaction transaction;
|
||||
public final PSBT psbt;
|
||||
public final BitcoinURI uri;
|
||||
public final String error;
|
||||
public final ExtendedKey extendedKey;
|
||||
public final OutputDescriptor outputDescriptor;
|
||||
public final List<Wallet> wallets;
|
||||
public final String payload;
|
||||
public final Throwable exception;
|
||||
|
||||
public Result(Transaction transaction) {
|
||||
this.transaction = transaction;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.error = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
@@ -192,7 +450,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.transaction = null;
|
||||
this.psbt = psbt;
|
||||
this.uri = null;
|
||||
this.error = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
@@ -200,7 +461,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = uri;
|
||||
this.error = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
@@ -208,15 +472,54 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = BitcoinURI.fromAddress(address);
|
||||
this.error = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(String error) {
|
||||
public Result(ExtendedKey extendedKey) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.error = error;
|
||||
this.extendedKey = extendedKey;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(OutputDescriptor outputDescriptor) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(List<Wallet> wallets) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = wallets;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(String payload) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = payload;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
@@ -224,8 +527,47 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.error = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.payload = null;
|
||||
this.exception = exception;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanException extends Exception {
|
||||
public ScanException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ScanException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ScanException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ScanException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class URException extends ScanException {
|
||||
public URException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public URException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public URException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public URException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu;
|
||||
import javafx.geometry.Pos;
|
||||
import org.controlsfx.control.decoration.Decorator;
|
||||
import org.controlsfx.control.decoration.GraphicDecoration;
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.sparrowwallet.sparrow.transaction;
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
@@ -33,7 +33,7 @@ public class ScriptContextMenu extends ContextMenu {
|
||||
});
|
||||
|
||||
getItems().add(copyvalue);
|
||||
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: sans-serif; -fx-font-size: 1em;");
|
||||
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
|
||||
|
||||
area.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
hoverChunk = null;
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@@ -10,7 +10,8 @@ import javafx.scene.layout.StackPane;
|
||||
public class SeedDisplayDialog extends Dialog<Void> {
|
||||
public SeedDisplayDialog(Keystore decryptedKeystore) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.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,9 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
public class TextAreaDialog extends Dialog<String> {
|
||||
private final TextArea textArea;
|
||||
@@ -20,11 +22,15 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
this.textArea = new TextArea(defaultValue);
|
||||
this.textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
this.textArea.setWrapText(true);
|
||||
this.textArea.getStyleClass().add("fixed-width");
|
||||
hbox.getChildren().add(textArea);
|
||||
HBox.setHgrow(this.textArea, Priority.ALWAYS);
|
||||
|
||||
this.defaultValue = defaultValue;
|
||||
|
||||
getDialogPane().setContent(hbox);
|
||||
dialogPane.setContent(hbox);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
dialogPane.getStyleClass().add("text-input-dialog");
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
@@ -35,6 +41,9 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? textArea.getText() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(700);
|
||||
dialogPane.setPrefHeight(400);
|
||||
}
|
||||
|
||||
public final TextArea getEditor() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -21,7 +21,7 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
protected HBox buttonBox;
|
||||
|
||||
public TitledDescriptionPane(String title, String description, String content, String imageUrl) {
|
||||
getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
getStyleClass().add("titled-description-pane");
|
||||
|
||||
setPadding(Insets.EMPTY);
|
||||
@@ -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;
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int MAX_PAYMENTS = 6;
|
||||
private static final double DIAGRAM_HEIGHT = 230.0;
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
|
||||
@@ -49,10 +61,12 @@ public class TransactionDiagram extends GridPane {
|
||||
Pane txPane = getTransactionPane();
|
||||
GridPane.setConstraints(txPane, 3, 0);
|
||||
|
||||
Pane outputsLinesPane = getOutputsLines();
|
||||
List<Payment> displayedPayments = getDisplayedPayments();
|
||||
|
||||
Pane outputsLinesPane = getOutputsLines(displayedPayments);
|
||||
GridPane.setConstraints(outputsLinesPane, 4, 0);
|
||||
|
||||
Pane outputsPane = getOutputsLabels();
|
||||
Pane outputsPane = getOutputsLabels(displayedPayments);
|
||||
GridPane.setConstraints(outputsPane, 5, 0);
|
||||
|
||||
getChildren().clear();
|
||||
@@ -62,11 +76,16 @@ public class TransactionDiagram extends GridPane {
|
||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
||||
|
||||
if(getPayjoinURI() != null) {
|
||||
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
|
||||
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
|
||||
}
|
||||
|
||||
if(selectedUtxos.size() > MAX_UTXOS) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
||||
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
||||
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
||||
if (utxos.size() < MAX_UTXOS) {
|
||||
if(utxos.size() < MAX_UTXOS - 1) {
|
||||
utxos.put(reference, selectedUtxos.get(reference));
|
||||
} else {
|
||||
additional.add(reference);
|
||||
@@ -80,6 +99,22 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private BitcoinURI getPayjoinURI() {
|
||||
for(Payment payment : walletTx.getPayments()) {
|
||||
try {
|
||||
Address address = payment.getAddress();
|
||||
BitcoinURI bitcoinURI = AppServices.getPayjoinURI(address);
|
||||
if(bitcoinURI != null) {
|
||||
return bitcoinURI;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Pane getInputsType(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
StackPane stackPane = new StackPane();
|
||||
|
||||
@@ -146,24 +181,39 @@ public class TransactionDiagram extends GridPane {
|
||||
WalletNode walletNode = displayedUtxos.get(input);
|
||||
String desc = getInputDescription(input);
|
||||
Label label = new Label(desc);
|
||||
label.getStyleClass().add("utxo-label");
|
||||
|
||||
Button excludeUtxoButton = new Button("");
|
||||
excludeUtxoButton.setGraphic(getExcludeGlyph());
|
||||
excludeUtxoButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ExcludeUtxoEvent(walletTx, input));
|
||||
});
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath() + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath().replace("m", "..") + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
label.getStyleClass().add("input-label");
|
||||
} else {
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
} else {
|
||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex)input;
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
} else {
|
||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
label.setTooltip(tooltip);
|
||||
|
||||
inputsBox.getChildren().add(label);
|
||||
@@ -195,11 +245,16 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
|
||||
int numUtxos = displayedUtxos.size();
|
||||
for(int i = 1; i <= numUtxos; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
}
|
||||
|
||||
curve.setStartX(0);
|
||||
double scaleFactor = (double)i / (numUtxos + 1);
|
||||
int nodeHeight = 17;
|
||||
@@ -224,7 +279,28 @@ public class TransactionDiagram extends GridPane {
|
||||
return value * (1.0 - scaleFactor) + additional;
|
||||
}
|
||||
|
||||
private Pane getOutputsLines() {
|
||||
private List<Payment> getDisplayedPayments() {
|
||||
List<Payment> payments = walletTx.getPayments();
|
||||
|
||||
if(payments.size() > MAX_PAYMENTS) {
|
||||
List<Payment> displayedPayments = new ArrayList<>();
|
||||
List<Payment> additional = new ArrayList<>();
|
||||
for(Payment payment : payments) {
|
||||
if(displayedPayments.size() < MAX_PAYMENTS - 1) {
|
||||
displayedPayments.add(payment);
|
||||
} else {
|
||||
additional.add(payment);
|
||||
}
|
||||
}
|
||||
|
||||
displayedPayments.add(new AdditionalPayment(additional));
|
||||
return displayedPayments;
|
||||
} else {
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
|
||||
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
@@ -238,7 +314,7 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
int numOutputs = (walletTx.getChangeNode() == null ? 2 : 3);
|
||||
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("output-line");
|
||||
@@ -263,26 +339,34 @@ public class TransactionDiagram extends GridPane {
|
||||
return pane;
|
||||
}
|
||||
|
||||
private Pane getOutputsLabels() {
|
||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||
VBox outputsBox = new VBox();
|
||||
outputsBox.setMaxWidth(150);
|
||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
String recipientDesc = walletTx.getRecipientAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, getSendGlyph());
|
||||
recipientLabel.getStyleClass().addAll("output-label", "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip("Send " + getSatsValue(walletTx.getRecipientAmount()) + " sats to\n" + walletTx.getRecipientAddress().toString());
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputsBox.getChildren().add(recipientLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
for(Payment payment : displayedPayments) {
|
||||
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
||||
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputsBox.getChildren().add(recipientLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
if(walletTx.getChangeNode() != null) {
|
||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath() + "\n" + walletTx.getChangeAddress().toString());
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString());
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
outputsBox.getChildren().add(changeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
@@ -294,6 +378,7 @@ public class TransactionDiagram extends GridPane {
|
||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
feeLabel.setTooltip(feeTooltip);
|
||||
outputsBox.getChildren().add(feeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
@@ -310,6 +395,8 @@ public class TransactionDiagram extends GridPane {
|
||||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
txPane.getChildren().add(txLabel);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
@@ -323,41 +410,77 @@ 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 getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
excludeGlyph.setFontSize(12);
|
||||
return excludeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getChangeGlyph() {
|
||||
Glyph changeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.COINS);
|
||||
public static Glyph getPaymentGlyph() {
|
||||
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
paymentGlyph.getStyleClass().add("payment-icon");
|
||||
paymentGlyph.setFontSize(12);
|
||||
return paymentGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getConsolidationGlyph() {
|
||||
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
|
||||
consolidationGlyph.getStyleClass().add("consolidation-icon");
|
||||
consolidationGlyph.setFontSize(12);
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getTxoGlyph() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getPayjoinGlyph() {
|
||||
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
payjoinGlyph.getStyleClass().add("payjoin-icon");
|
||||
payjoinGlyph.setFontSize(12);
|
||||
return payjoinGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeGlyph() {
|
||||
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
changeGlyph.getStyleClass().add("change-icon");
|
||||
changeGlyph.setFontSize(12);
|
||||
return changeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getFeeGlyph() {
|
||||
Glyph feeGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.HAND_HOLDING);
|
||||
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
|
||||
feeGlyph.getStyleClass().add("fee-icon");
|
||||
feeGlyph.setFontSize(12);
|
||||
return feeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getWarningGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private Glyph getLockGlyph() {
|
||||
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.LOCK);
|
||||
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
|
||||
lockGlyph.getStyleClass().add("lock-icon");
|
||||
lockGlyph.setFontSize(12);
|
||||
return lockGlyph;
|
||||
}
|
||||
|
||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public PayjoinBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Payjoin input";
|
||||
}
|
||||
}
|
||||
|
||||
private static class AdditionalBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final List<BlockTransactionHashIndex> additionalInputs;
|
||||
|
||||
@@ -375,4 +498,17 @@ public class TransactionDiagram extends GridPane {
|
||||
return additionalInputs;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
public AdditionalPayment(List<Payment> additionalPayments) {
|
||||
super(null, additionalPayments.size() + " more...", additionalPayments.stream().map(Payment::getAmount).mapToLong(v -> v).sum(), false);
|
||||
this.additionalPayments = additionalPayments;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -23,12 +23,13 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
|
||||
|
||||
public TransactionIdDialog() {
|
||||
this.txid = (CustomTextField) TextFields.createClearableTextField();
|
||||
txid.setFont(Font.font ("Courier", txid.getFont().getSize()));
|
||||
txid.setFont(AppServices.getMonospaceFont());
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Load Transaction");
|
||||
dialogPane.setHeaderText("Enter the transaction ID:");
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(550);
|
||||
dialogPane.setPrefHeight(200);
|
||||
|
||||
@@ -39,7 +39,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
public void update(WalletUtxosEntry walletUtxosEntry) {
|
||||
List<Data<String, Number>> utxoDataList = walletUtxosEntry.getChildren().stream()
|
||||
.map(entry -> new XYChart.Data<>(getCategoryName(entry), (Number)entry.getValue(), entry))
|
||||
.sorted((o1, o2) -> (int) (o2.getYValue().longValue() - o1.getYValue().longValue()))
|
||||
.sorted((o1, o2) -> Long.compare(o2.getYValue().longValue(), o1.getYValue().longValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
totalUtxos = utxoDataList.size();
|
||||
@@ -86,11 +86,10 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
for(int i = 0; i < utxoSeries.getData().size(); i++) {
|
||||
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
|
||||
Node bar = lookup(".data" + i);
|
||||
if(bar != null) {
|
||||
if(data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) {
|
||||
bar.getStyleClass().add("selected");
|
||||
} else if(data.getExtraValue() == null && entries.size() == totalUtxos) {
|
||||
|
||||
if((data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) || (data.getExtraValue() == null && entries.size() == totalUtxos)) {
|
||||
Node bar = lookup(".data" + i);
|
||||
if(bar != null) {
|
||||
bar.getStyleClass().add("selected");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@ 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.AppServices;
|
||||
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 com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -26,6 +23,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
});
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
@@ -34,7 +32,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
stackPane.getChildren().add(anchorPane);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setPrefHeight(280);
|
||||
scrollPane.setPrefHeight(400);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
@@ -43,9 +41,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum());
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new ColdcardMultisig(), new Electrum());
|
||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
@@ -60,7 +58,7 @@ public class WalletExportDialog 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(480);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
}
|
||||
|
||||
@@ -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.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
@@ -21,6 +22,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
});
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
@@ -29,16 +31,22 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
stackPane.getChildren().add(anchorPane);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setPrefHeight(280);
|
||||
scrollPane.setPrefHeight(420);
|
||||
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(), new CoboVaultSinglesig());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
@@ -47,7 +55,7 @@ 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(500);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
@@ -22,10 +22,11 @@ public class WalletNameDialog extends Dialog<String> {
|
||||
public WalletNameDialog() {
|
||||
this.name = (CustomTextField)TextFields.createClearableTextField();
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Wallet Name");
|
||||
dialogPane.setHeaderText("Enter a name for this wallet:");
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
@@ -21,6 +21,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
private final CustomPasswordField password;
|
||||
private final CustomPasswordField passwordConfirm;
|
||||
private final CheckBox backupExisting;
|
||||
private final CheckBox changePassword;
|
||||
|
||||
public WalletPasswordDialog(PasswordRequirement requirement) {
|
||||
this(null, requirement);
|
||||
@@ -31,11 +32,13 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.backupExisting = new CheckBox("Backup existing wallet first");
|
||||
this.changePassword = new CheckBox("Change password");
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
|
||||
dialogPane.setHeaderText(walletName != null ? requirement.description.substring(0, requirement.description.length() - 1) + " for " + walletName + ":" : requirement.description);
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(260);
|
||||
@@ -54,6 +57,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
backupExisting.setSelected(true);
|
||||
}
|
||||
|
||||
if(requirement == PasswordRequirement.UPDATE_SET) {
|
||||
content.getChildren().add(changePassword);
|
||||
}
|
||||
|
||||
dialogPane.setContent(content);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -69,12 +76,12 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
if(requirement != PasswordRequirement.UPDATE_NEW) {
|
||||
if(requirement != PasswordRequirement.UPDATE_NEW && requirement != PasswordRequirement.UPDATE_CHANGE) {
|
||||
passwordConfirm.setVisible(false);
|
||||
passwordConfirm.setManaged(false);
|
||||
}
|
||||
|
||||
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY) {
|
||||
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_CHANGE) {
|
||||
password.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.isEmpty()) {
|
||||
okButton.setText("No Password");
|
||||
@@ -99,11 +106,16 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
return backupExisting.isSelected();
|
||||
}
|
||||
|
||||
public boolean isChangePassword() {
|
||||
return changePassword.isSelected();
|
||||
}
|
||||
|
||||
public enum PasswordRequirement {
|
||||
LOAD("Please enter the wallet password:", "Unlock"),
|
||||
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
|
||||
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for none:", "No Password"),
|
||||
UPDATE_SET("Please re-enter the wallet password:", "Verify Password");
|
||||
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
|
||||
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for none:", "No Password");
|
||||
|
||||
private final String description;
|
||||
private final String okButtonText;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import javafx.application.HostServices;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -27,7 +27,8 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||
|
||||
setTitle("Welcome to Sparrow");
|
||||
dialogPane.setHeaderText("Welcome to Sparrow!");
|
||||
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(480);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
private final List<String> serverVersion;
|
||||
private final String serverBanner;
|
||||
private final int blockHeight;
|
||||
private final BlockHeader blockHeader;
|
||||
private final Double minimumRelayFeeRate;
|
||||
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates) {
|
||||
super(targetBlockFeeRates);
|
||||
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader, Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double minimumRelayFeeRate) {
|
||||
super(targetBlockFeeRates, mempoolRateSizes);
|
||||
this.serverVersion = serverVersion;
|
||||
this.serverBanner = serverBanner;
|
||||
this.blockHeight = blockHeight;
|
||||
this.blockHeader = blockHeader;
|
||||
this.minimumRelayFeeRate = minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public List<String> getServerVersion() {
|
||||
@@ -34,4 +38,8 @@ public class ConnectionEvent extends FeeRatesUpdatedEvent {
|
||||
public BlockHeader getBlockHeader() {
|
||||
return blockHeader;
|
||||
}
|
||||
|
||||
public Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
|
||||
public class ExcludeUtxoEvent {
|
||||
private final WalletTransaction walletTransaction;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
|
||||
public ExcludeUtxoEvent(WalletTransaction walletTransaction, BlockTransactionHashIndex utxo) {
|
||||
this.walletTransaction = walletTransaction;
|
||||
this.utxo = utxo;
|
||||
}
|
||||
|
||||
public WalletTransaction getWalletTransaction() {
|
||||
return walletTransaction;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
|
||||
public class FeeRatesSelectionChangedEvent {
|
||||
private final FeeRatesSelection feeRatesSelection;
|
||||
|
||||
public FeeRatesSelectionChangedEvent(FeeRatesSelection feeRatesSelection) {
|
||||
this.feeRatesSelection = feeRatesSelection;
|
||||
}
|
||||
|
||||
public FeeRatesSelection getFeeRateSelection() {
|
||||
return feeRatesSelection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
|
||||
public class FeeRatesSourceChangedEvent {
|
||||
private final FeeRatesSource feeRatesSource;
|
||||
|
||||
public FeeRatesSourceChangedEvent(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource = feeRatesSource;
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRateSource() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FeeRatesUpdatedEvent {
|
||||
private final Map<Integer, Double> targetBlockFeeRates;
|
||||
private final Set<MempoolRateSize> mempoolRateSizes;
|
||||
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates) {
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||
this.mempoolRateSizes = mempoolRateSizes;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public Set<MempoolRateSize> getMempoolRateSizes() {
|
||||
return mempoolRateSizes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
|
||||
import java.util.Currency;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class HideEmptyUsedAddressesStatusEvent {
|
||||
private final boolean hideEmptyUsedAddresses;
|
||||
|
||||
public HideEmptyUsedAddressesStatusEvent(boolean hideEmptyUsedAddresses) {
|
||||
this.hideEmptyUsedAddresses = hideEmptyUsedAddresses;
|
||||
}
|
||||
|
||||
public boolean isHideEmptyUsedAddresses() {
|
||||
return hideEmptyUsedAddresses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
/**
|
||||
* This event is used by the DeviceSignMessageDialog to indicate that a USB device has signed a message
|
||||
*
|
||||
*/
|
||||
public class MessageSignedEvent {
|
||||
private final Wallet wallet;
|
||||
private final String signature;
|
||||
|
||||
public MessageSignedEvent(Wallet wallet, String signature) {
|
||||
this.wallet = wallet;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,25 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class OpenWalletsEvent {
|
||||
private final Window window;
|
||||
private final Map<Wallet, Storage> walletsMap;
|
||||
|
||||
public OpenWalletsEvent(Map<Wallet, Storage> walletsMap) {
|
||||
public OpenWalletsEvent(Window window, Map<Wallet, Storage> walletsMap) {
|
||||
this.window = window;
|
||||
this.walletsMap = walletsMap;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public List<Wallet> getWallets() {
|
||||
return new ArrayList<>(walletsMap.keySet());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class OpenWalletsNewWindowsStatusEvent {
|
||||
private final boolean openWalletsInNewWindows;
|
||||
|
||||
public OpenWalletsNewWindowsStatusEvent(boolean openWalletsInNewWindows) {
|
||||
this.openWalletsInNewWindows = openWalletsInNewWindows;
|
||||
}
|
||||
|
||||
public boolean isOpenWalletsInNewWindows() {
|
||||
return openWalletsInNewWindows;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Event class used to request the QRScanDialog is opened
|
||||
*/
|
||||
public class RequestQRScanEvent {
|
||||
//Empty event class used to request the QRScanDialog is opened
|
||||
private final Window window;
|
||||
|
||||
public RequestQRScanEvent(Window window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Event class used to request the transaction open file dialog
|
||||
*/
|
||||
public class RequestTransactionOpenEvent {
|
||||
//Empty event class used to request the transaction open file dialog
|
||||
private final Window window;
|
||||
|
||||
public RequestTransactionOpenEvent(Window window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Event class used to request the wallet open dialog
|
||||
*/
|
||||
public class RequestWalletOpenEvent {
|
||||
//Empty event class used to request the wallet open dialog
|
||||
private final Window window;
|
||||
|
||||
public RequestWalletOpenEvent(Window window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SendActionEvent {
|
||||
private final List<HashIndexEntry> utxoEntries;
|
||||
private final Wallet wallet;
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public SendActionEvent(List<HashIndexEntry> utxoEntries) {
|
||||
this.utxoEntries = utxoEntries;
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
public List<HashIndexEntry> getUtxoEntries() {
|
||||
return utxoEntries;
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SpendUtxoEvent {
|
||||
private final List<HashIndexEntry> utxoEntries;
|
||||
private final Wallet wallet;
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
private final List<Payment> payments;
|
||||
private final Long fee;
|
||||
private final boolean includeMempoolInputs;
|
||||
|
||||
public SpendUtxoEvent(List<HashIndexEntry> utxoEntries) {
|
||||
this.utxoEntries = utxoEntries;
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, false);
|
||||
}
|
||||
|
||||
public List<HashIndexEntry> getUtxoEntries() {
|
||||
return utxoEntries;
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeMempoolInputs) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.fee = fee;
|
||||
this.includeMempoolInputs = includeMempoolInputs;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public Long getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
public boolean isIncludeMempoolInputs() {
|
||||
return includeMempoolInputs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
|
||||
public class ThemeChangedEvent {
|
||||
private final Theme theme;
|
||||
|
||||
public ThemeChangedEvent(Theme theme) {
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class TorStatusEvent {
|
||||
private final String status;
|
||||
|
||||
public TorStatusEvent(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class VersionCheckStatusEvent {
|
||||
private final boolean enabled;
|
||||
|
||||
public VersionCheckStatusEvent(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class VersionUpdatedEvent {
|
||||
private final String version;
|
||||
|
||||
public VersionUpdatedEvent(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,43 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class ViewPSBTEvent {
|
||||
private final Window window;
|
||||
private final String label;
|
||||
private final File file;
|
||||
private final PSBT psbt;
|
||||
private final TransactionView initialView;
|
||||
private final Integer initialIndex;
|
||||
|
||||
public ViewPSBTEvent(String label, PSBT psbt) {
|
||||
this(label, psbt, TransactionView.HEADERS, null);
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt) {
|
||||
this(window, label, file, psbt, TransactionView.HEADERS, null);
|
||||
}
|
||||
|
||||
public ViewPSBTEvent(String label, PSBT psbt, TransactionView initialView, Integer initialIndex) {
|
||||
public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, TransactionView initialView, Integer initialIndex) {
|
||||
this.window = window;
|
||||
this.label = label;
|
||||
this.file = file;
|
||||
this.psbt = psbt;
|
||||
this.initialView = initialView;
|
||||
this.initialIndex = initialIndex;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public PSBT getPsbt() {
|
||||
return psbt;
|
||||
}
|
||||
|
||||
@@ -3,26 +3,33 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import javafx.stage.Window;
|
||||
|
||||
public class ViewTransactionEvent {
|
||||
private final Window window;
|
||||
private final BlockTransaction blockTransaction;
|
||||
private final TransactionView initialView;
|
||||
private final Integer initialIndex;
|
||||
|
||||
public ViewTransactionEvent(BlockTransaction blockTransaction) {
|
||||
this(blockTransaction, TransactionView.HEADERS, null);
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction) {
|
||||
this(window, blockTransaction, TransactionView.HEADERS, null);
|
||||
}
|
||||
|
||||
public ViewTransactionEvent(BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) {
|
||||
this(blockTransaction, hashIndexEntry.getType().equals(HashIndexEntry.Type.INPUT) ? TransactionView.INPUT : TransactionView.OUTPUT, (int)hashIndexEntry.getHashIndex().getIndex());
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) {
|
||||
this(window, blockTransaction, hashIndexEntry.getType().equals(HashIndexEntry.Type.INPUT) ? TransactionView.INPUT : TransactionView.OUTPUT, (int)hashIndexEntry.getHashIndex().getIndex());
|
||||
}
|
||||
|
||||
public ViewTransactionEvent(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
this.window = window;
|
||||
this.blockTransaction = blockTransaction;
|
||||
this.initialView = initialView;
|
||||
this.initialIndex = initialIndex;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return blockTransaction;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
public class ViewWalletEvent {
|
||||
private final Window window;
|
||||
private final Wallet wallet;
|
||||
private final Storage storage;
|
||||
|
||||
public ViewWalletEvent(Window window, Wallet wallet, Storage storage) {
|
||||
this.window = window;
|
||||
this.wallet = wallet;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
/**
|
||||
* Used to indicate that the display configuration of wallet addresses has been updated
|
||||
*/
|
||||
public class WalletAddressesStatusEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WalletAddressesStatusEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryStatusEvent {
|
||||
private final Wallet wallet;
|
||||
private final boolean loaded;
|
||||
private final String statusMessage;
|
||||
private final String errorMessage;
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet, boolean loaded) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = loaded;
|
||||
this.statusMessage = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet,boolean loaded, String statusMessage) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = false;
|
||||
this.statusMessage = statusMessage;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet,String errorMessage) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = false;
|
||||
this.statusMessage = null;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return !loaded;
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletUtxoStatusChangedEvent extends WalletDataChangedEvent {
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||
super(wallet);
|
||||
this.utxo = utxo;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
}
|
||||
@@ -23,22 +23,28 @@ public class FontAwesome5 extends GlyphFont {
|
||||
CHECK_CIRCLE('\uf058'),
|
||||
CIRCLE('\uf111'),
|
||||
COINS('\uf51e'),
|
||||
EXCHANGE_ALT('\uf362'),
|
||||
EXCLAMATION_CIRCLE('\uf06a'),
|
||||
EXCLAMATION_TRIANGLE('\uf071'),
|
||||
EXTERNAL_LINK_ALT('\uf35d'),
|
||||
ELLIPSIS_H('\uf141'),
|
||||
EYE('\uf06e'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
KEY('\uf084'),
|
||||
LAPTOP('\uf109'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
QRCODE('\uf029'),
|
||||
QUESTION_CIRCLE('\uf059'),
|
||||
RANDOM('\uf074'),
|
||||
REPLY_ALL('\uf122'),
|
||||
SATELLITE_DISH('\uf7c0'),
|
||||
SD_CARD('\uf7c2'),
|
||||
SEARCH('\uf002'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
TOOLS('\uf7d9'),
|
||||
UNDO('\uf0e2'),
|
||||
WALLET('\uf555');
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class CoboVaultMultisig extends ColdcardMultisig {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Cobo Vault Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.COBO_VAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
keystore.setLabel("Cobo Vault");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created by using the Multisig Wallet > Show/Export XPUB feature on your Cobo Vault.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = super.importWallet(inputStream, password);
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Cobo Vault"));
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file or QR created by using the Multisig Wallet > Create Multisig Wallet feature on your Cobo Vault.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file that can be read by your Cobo Vault using the Multisig Wallet > Import Multisig Wallet feature.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Cobo Vault";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created by using the Settings > Watch-Only Wallet > Generic Wallet > Export Wallet feature on your Cobo Vault.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.COBO_VAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream), CoboVaultSinglesigKeystore.class);
|
||||
|
||||
if(coboKeystore.MasterFingerprint == null || coboKeystore.AccountKeyPath == null || coboKeystore.ExtPubKey == null) {
|
||||
throw new ImportException("Not a valid " + getName() + " keystore export");
|
||||
}
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(coboKeystore.ExtPubKey);
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
}
|
||||
|
||||
return keystore;
|
||||
} catch (Exception e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return getKeystoreImportDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new ImportException("Imported Cobo Vault wallet was invalid: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class CoboVaultSinglesigKeystore {
|
||||
public String ExtPubKey;
|
||||
public String MasterFingerprint;
|
||||
public String AccountKeyPath;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
@@ -19,8 +18,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ColdcardMultisig implements WalletImport, KeystoreFileImport, WalletExport {
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Coldcard Multisig";
|
||||
@@ -40,7 +37,14 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
|
||||
if(scriptType.equals(ScriptType.P2SH)) {
|
||||
if(cck.xpub != null && cck.path != null) {
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
}
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
@@ -63,6 +67,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
public String p2wsh_p2sh;
|
||||
public String p2wsh_deriv;
|
||||
public String p2wsh;
|
||||
public String xpub;
|
||||
public String path;
|
||||
public String xfp;
|
||||
}
|
||||
|
||||
@@ -82,7 +88,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
|
||||
int threshold = 2;
|
||||
ScriptType scriptType = null;
|
||||
ScriptType scriptType = ScriptType.P2SH;
|
||||
String derivation = null;
|
||||
|
||||
try {
|
||||
@@ -153,7 +159,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
throw new ExportException("Coldcard multisig import requires a multisig wallet");
|
||||
throw new ExportException(getName() + " import requires a multisig wallet");
|
||||
}
|
||||
|
||||
boolean multipleDerivations = false;
|
||||
@@ -167,7 +173,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
writer.append("# Coldcard Multisig setup file (created by Sparrow)\n");
|
||||
writer.append("# " + getName() + " setup file (created by Sparrow)\n");
|
||||
writer.append("#\n");
|
||||
writer.append("Name: ").append(wallet.getName()).append("\n");
|
||||
writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
|
||||
@@ -203,4 +209,14 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
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.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
@@ -16,7 +16,7 @@ import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
public class ColdcardSinglesig implements KeystoreFileImport {
|
||||
public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Coldcard";
|
||||
@@ -37,6 +37,16 @@ public class ColdcardSinglesig implements KeystoreFileImport {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
@@ -59,7 +69,7 @@ public class ColdcardSinglesig implements KeystoreFileImport {
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").toUpperCase());
|
||||
if(ckScriptType.equals(scriptType)) {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel("Coldcard");
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv));
|
||||
@@ -77,6 +87,31 @@ public class ColdcardSinglesig implements KeystoreFileImport {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return getKeystoreImportDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new ImportException("Imported Coldcard wallet was invalid: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private static class ColdcardKeystore {
|
||||
public String deriv;
|
||||
public String name;
|
||||
|
||||
@@ -3,6 +3,10 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -18,11 +22,18 @@ public class Config {
|
||||
|
||||
private Mode mode;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private FeeRatesSource feeRatesSource;
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean groupByAddress = true;
|
||||
private boolean includeMempoolChange = true;
|
||||
private boolean notifyNewTransactions = true;
|
||||
private boolean checkNewVersions = true;
|
||||
private Theme theme;
|
||||
private boolean openWalletsInNewWindows = false;
|
||||
private boolean hideEmptyUsedAddresses = false;
|
||||
private boolean showTransactionHex = true;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
@@ -41,7 +52,12 @@ public class Config {
|
||||
}
|
||||
|
||||
private static File getConfigFile() {
|
||||
return new File(Storage.getSparrowDir(), CONFIG_FILENAME);
|
||||
File sparrowDir = Storage.getSparrowDir();
|
||||
if(!sparrowDir.exists()) {
|
||||
sparrowDir.mkdirs();
|
||||
}
|
||||
|
||||
return new File(sparrowDir, CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
private static Config load() {
|
||||
@@ -90,6 +106,24 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRatesSource() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
|
||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource = feeRatesSource;
|
||||
flush();
|
||||
}
|
||||
|
||||
public FeeRatesSelection getFeeRatesSelection() {
|
||||
return feeRatesSelection;
|
||||
}
|
||||
|
||||
public void setFeeRatesSelection(FeeRatesSelection feeRatesSelection) {
|
||||
this.feeRatesSelection = feeRatesSelection;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Currency getFiatCurrency() {
|
||||
return fiatCurrency;
|
||||
}
|
||||
@@ -135,6 +169,51 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isCheckNewVersions() {
|
||||
return checkNewVersions;
|
||||
}
|
||||
|
||||
public void setCheckNewVersions(boolean checkNewVersions) {
|
||||
this.checkNewVersions = checkNewVersions;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
this.theme = theme;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isOpenWalletsInNewWindows() {
|
||||
return openWalletsInNewWindows;
|
||||
}
|
||||
|
||||
public void setOpenWalletsInNewWindows(boolean openWalletsInNewWindows) {
|
||||
this.openWalletsInNewWindows = openWalletsInNewWindows;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isHideEmptyUsedAddresses() {
|
||||
return hideEmptyUsedAddresses;
|
||||
}
|
||||
|
||||
public void setHideEmptyUsedAddresses(boolean hideEmptyUsedAddresses) {
|
||||
this.hideEmptyUsedAddresses = hideEmptyUsedAddresses;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowTransactionHex() {
|
||||
return showTransactionHex;
|
||||
}
|
||||
|
||||
public void setShowTransactionHex(boolean showTransactionHex) {
|
||||
this.showTransactionHex = showTransactionHex;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
return recentWalletFiles;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user