mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcb83f8bfa | ||
|
|
f187603ec3 | ||
|
|
8d7308bc37 | ||
|
|
e44d1393f5 | ||
|
|
33ba472843 | ||
|
|
faa81f2273 | ||
|
|
0646c8aa28 | ||
|
|
deb47ca002 | ||
|
|
ec131bb8da | ||
|
|
31f287125f | ||
|
|
e131f645f6 | ||
|
|
eabc0da6d5 | ||
|
|
49573d1075 | ||
|
|
17093dbf72 | ||
|
|
c2b5b24702 | ||
|
|
65f1fa7cf8 | ||
|
|
cbee341544 | ||
|
|
95b1aa8e48 | ||
|
|
af89be96e5 | ||
|
|
fad960c192 | ||
|
|
1adeef04db | ||
|
|
47f925b677 | ||
|
|
5db3096386 | ||
|
|
62e98b0090 | ||
|
|
76490604ac | ||
|
|
783733b9d3 | ||
|
|
041b5aa34c | ||
|
|
33d23e9ea5 | ||
|
|
b3f6cc88f0 | ||
|
|
b912aa2eb9 | ||
|
|
d894343457 | ||
|
|
fb1e1cefda | ||
|
|
d960bdce96 | ||
|
|
fb679c0199 | ||
|
|
9ecfe0e94f | ||
|
|
1bc2f7d69f | ||
|
|
6e4d308c79 | ||
|
|
afb6037449 | ||
|
|
369983748d | ||
|
|
0d16c87b40 | ||
|
|
b59a65dcfe | ||
|
|
87cc28e0a4 | ||
|
|
1187925543 | ||
|
|
cd4edab4ae | ||
|
|
daf320f36b | ||
|
|
f6ff92865b | ||
|
|
d420c71673 | ||
|
|
07101b3ca0 | ||
|
|
00f7f3f5b3 | ||
|
|
5d2c133401 | ||
|
|
7b0dfd66a7 | ||
|
|
83719e7df2 | ||
|
|
f1b246f0b0 | ||
|
|
599880ea5c | ||
|
|
d625bab02e | ||
|
|
1676676e06 | ||
|
|
f7e603118f | ||
|
|
f6fd889712 | ||
|
|
21d91d3d10 | ||
|
|
f1cddc28e7 | ||
|
|
1887e1c7b0 | ||
|
|
3e870f362d | ||
|
|
665d70b845 | ||
|
|
c2cbe62a5a | ||
|
|
c6b6e74515 | ||
|
|
8ddc494b53 | ||
|
|
33f439f49a | ||
|
|
d68ab40c94 | ||
|
|
31346e2afa | ||
|
|
c407a41475 | ||
|
|
8baa8e2e96 | ||
|
|
72adbe44a7 | ||
|
|
dd64c18c21 | ||
|
|
2354b061a9 | ||
|
|
a5050117a3 | ||
|
|
f245b57022 | ||
|
|
d3752a856b | ||
|
|
fe7dba6d83 | ||
|
|
2d0a94d024 | ||
|
|
41146310d6 | ||
|
|
a167f6aedb | ||
|
|
0fed7c45ee | ||
|
|
5a0df265bc | ||
|
|
646b8b0e65 | ||
|
|
c9b40b1973 | ||
|
|
9ec5b6ce26 | ||
|
|
93893111c6 | ||
|
|
3600d32ffd | ||
|
|
1e0855c11d | ||
|
|
15cb028951 | ||
|
|
e178168bec | ||
|
|
5696e00cb5 | ||
|
|
594a873f20 | ||
|
|
da30d4223a | ||
|
|
2441b4d7c3 | ||
|
|
cc739a71e9 | ||
|
|
5f98eb9eb9 | ||
|
|
5aa25b98c3 | ||
|
|
5058cd283d | ||
|
|
af6171692b | ||
|
|
3c631fa653 | ||
|
|
10a796098b | ||
|
|
8ac642b09c | ||
|
|
33d9f260c4 | ||
|
|
86247c6440 | ||
|
|
c39425ed3b | ||
|
|
e5e94c3ea6 | ||
|
|
9ba4458f48 | ||
|
|
a9fd7c263f | ||
|
|
6ef1313137 | ||
|
|
8e66db0237 | ||
|
|
6da98befe1 | ||
|
|
6b4c301458 | ||
|
|
86ff7b8cf9 | ||
|
|
a805d9e036 | ||
|
|
f0bfc44e72 | ||
|
|
0fad93524e | ||
|
|
c1fc8712d5 | ||
|
|
5d674b7e91 | ||
|
|
d1a353ae53 | ||
|
|
08ec158d19 | ||
|
|
2e8112cba0 | ||
|
|
c108741b6f | ||
|
|
4450d625dd | ||
|
|
2e1ee0c5b2 | ||
|
|
1c3c2d8089 | ||
|
|
a4991064af | ||
|
|
6ea6f4b5d2 | ||
|
|
210d52c001 | ||
|
|
fcbed8795f | ||
|
|
0d9e798bb7 | ||
|
|
9d0c35bc75 | ||
|
|
f3c44e6f3e | ||
|
|
14d0437424 | ||
|
|
d2934c94c5 | ||
|
|
2e847199f5 | ||
|
|
c9d1650ed4 | ||
|
|
da74089969 | ||
|
|
d1ac5b076e | ||
|
|
e1564217ed | ||
|
|
2ef66d504f | ||
|
|
f0bd07b4b7 | ||
|
|
e43b783664 | ||
|
|
2d5e24366c | ||
|
|
195854fb6f | ||
|
|
249a01c208 |
@@ -15,11 +15,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
- name: Set up JDK 22.0.2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
java-version: '22.0.2'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Build with Gradle
|
||||
|
||||
@@ -64,10 +64,12 @@ Usage: sparrow [options]
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
Note that testnet currently refers to testnet3.
|
||||
|
||||
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
@@ -83,7 +85,7 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
+34
-89
@@ -3,11 +3,11 @@ import java.awt.GraphicsEnvironment
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.8.3'
|
||||
def sparrowVersion = '2.0.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -53,7 +53,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "18"
|
||||
version = headless ? "18" : "22"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.40')
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
@@ -160,7 +160,7 @@ processResources {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -212,11 +212,9 @@ jlink {
|
||||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
|
||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -248,6 +246,8 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
|
||||
if(os.windows) {
|
||||
@@ -271,11 +271,11 @@ jlink {
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerType = "exe"
|
||||
installerType = "msi"
|
||||
}
|
||||
if(os.linux) {
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', 'src/main/deploy/package/linux-headless/']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
@@ -290,6 +290,16 @@ jlink {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jlink.finalizedBy "addUserWritePermission"
|
||||
|
||||
task addUserWritePermission(type: Exec) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
@@ -297,6 +307,7 @@ task removeGroupWritePermission(type: Exec) {
|
||||
task packageZipDistribution(type: Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
preserveFileTimestamps = os.macOsX
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
@@ -491,65 +502,21 @@ extraJavaModuleInfo {
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
requires('org.bouncycastle.provider')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('ch.qos.logback.classic')
|
||||
requires('org.json')
|
||||
requires('io.reactivex.rxjava2')
|
||||
exports('com.samourai.http.client')
|
||||
exports('com.samourai.tor.client')
|
||||
exports('com.samourai.wallet.api.backend')
|
||||
exports('com.samourai.wallet.api.backend.beans')
|
||||
exports('com.samourai.wallet.client.indexHandler')
|
||||
exports('com.samourai.wallet.hd')
|
||||
exports('com.samourai.wallet.util')
|
||||
exports('com.samourai.wallet.bip47.rpc')
|
||||
exports('com.samourai.wallet.bip47.rpc.java')
|
||||
exports('com.samourai.wallet.cahoots')
|
||||
exports('com.samourai.wallet.cahoots.psbt')
|
||||
exports('com.samourai.wallet.cahoots.stonewallx2')
|
||||
exports('com.samourai.soroban.cahoots')
|
||||
exports('com.samourai.soroban.client')
|
||||
exports('com.samourai.soroban.client.cahoots')
|
||||
exports('com.samourai.soroban.client.meeting')
|
||||
exports('com.samourai.soroban.client.rpc')
|
||||
exports('com.samourai.wallet.send')
|
||||
exports('com.samourai.whirlpool.client.event')
|
||||
exports('com.samourai.whirlpool.client.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
|
||||
exports('com.samourai.whirlpool.client.whirlpool')
|
||||
exports('com.samourai.whirlpool.client.whirlpool.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.pool')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxo')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.supplier')
|
||||
exports('com.samourai.whirlpool.client.mix.handler')
|
||||
exports('com.samourai.whirlpool.client.mix.listener')
|
||||
exports('com.samourai.whirlpool.protocol.beans')
|
||||
exports('com.samourai.whirlpool.protocol.rest')
|
||||
exports('com.samourai.whirlpool.client.tx0')
|
||||
exports('com.samourai.wallet.segwit.bech32')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.chain')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
exports('com.sparrowwallet.nightjar.http')
|
||||
exports('com.sparrowwallet.nightjar.stomp')
|
||||
exports('com.sparrowwallet.nightjar.tor')
|
||||
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
|
||||
exports('org.apache.commons.codec')
|
||||
}
|
||||
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
|
||||
exports('com.zeroleak.throwingsupplier')
|
||||
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
|
||||
exports('ch.qos.logback.core')
|
||||
}
|
||||
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
|
||||
exports('com.squareup.okhttp')
|
||||
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.datatype.jsr310')
|
||||
}
|
||||
module('json-20240205.jar', 'org.json', '20240205') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||
exports('okio')
|
||||
@@ -557,22 +524,12 @@ extraJavaModuleInfo {
|
||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||
exports('com.auth0.jwt')
|
||||
}
|
||||
module('json-20180130.jar', 'org.json', '1.0') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
||||
exports('com.google.protobuf')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
@@ -705,18 +662,6 @@ extraJavaModuleInfo {
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
|
||||
exports('org.pgpainless')
|
||||
exports('org.pgpainless.key')
|
||||
exports('org.pgpainless.key.parsing')
|
||||
exports('org.pgpainless.decryption_verification')
|
||||
exports('org.pgpainless.exception')
|
||||
exports('org.pgpainless.signature')
|
||||
exports('org.pgpainless.util')
|
||||
requires('org.bouncycastle.provider')
|
||||
requires('org.bouncycastle.pg')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||
exports('com.jcraft.jzlib')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||
implementation 'org.ow2.asm:asm:9.6'
|
||||
}
|
||||
|
||||
@@ -20,5 +22,9 @@ gradlePlugin {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2018, 2020, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* 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 HOLDER 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.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum JavaFXModule {
|
||||
|
||||
BASE,
|
||||
GRAPHICS(BASE),
|
||||
CONTROLS(BASE, GRAPHICS),
|
||||
FXML(BASE, GRAPHICS),
|
||||
MEDIA(BASE, GRAPHICS),
|
||||
SWING(BASE, GRAPHICS),
|
||||
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
||||
|
||||
static final String PREFIX_MODULE = "javafx.";
|
||||
private static final String PREFIX_ARTIFACT = "javafx-";
|
||||
|
||||
private List<JavaFXModule> dependentModules;
|
||||
|
||||
JavaFXModule(JavaFXModule...dependentModules) {
|
||||
this.dependentModules = List.of(dependentModules);
|
||||
}
|
||||
|
||||
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
||||
return Stream.of(JavaFXModule.values())
|
||||
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getModuleJarFileName() {
|
||||
return getModuleName() + ".jar";
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
||||
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
||||
return p.matcher(jarFileName).matches();
|
||||
}
|
||||
|
||||
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
||||
validateModules(moduleNames);
|
||||
|
||||
return moduleNames.stream()
|
||||
.map(JavaFXModule::fromModuleName)
|
||||
.flatMap(Optional::stream)
|
||||
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static void validateModules(List<String> moduleNames) {
|
||||
var invalidModules = moduleNames.stream()
|
||||
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (! invalidModules.isEmpty()) {
|
||||
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
||||
}
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getDependentModules() {
|
||||
return dependentModules;
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getMavenDependencies() {
|
||||
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
||||
dependencies.add(0, this);
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* 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 HOLDER 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.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
||||
|
||||
public class JavaFXOptions {
|
||||
|
||||
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
||||
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
||||
|
||||
private final Project project;
|
||||
private final JavaFXPlatform platform;
|
||||
|
||||
private String version = "16";
|
||||
private String sdk;
|
||||
private String configuration = "implementation";
|
||||
private String lastUpdatedConfiguration;
|
||||
private List<String> modules = new ArrayList<>();
|
||||
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
||||
|
||||
public JavaFXOptions(Project project) {
|
||||
this.project = project;
|
||||
this.platform = JavaFXPlatform.detect(project);
|
||||
}
|
||||
|
||||
public JavaFXPlatform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the JavaFX modules will be taken from this local
|
||||
* repository, and not from Maven Central
|
||||
* @param sdk, the path to the local JavaFX SDK folder
|
||||
*/
|
||||
public void setSdk(String sdk) {
|
||||
this.sdk = sdk;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getSdk() {
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/** Set the configuration name for dependencies, e.g.
|
||||
* 'implementation', 'compileOnly' etc.
|
||||
* @param configuration The configuration name for dependencies
|
||||
*/
|
||||
public void setConfiguration(String configuration) {
|
||||
this.configuration = configuration;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public List<String> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void setModules(List<String> modules) {
|
||||
this.modules = modules;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public void modules(String...moduleNames) {
|
||||
setModules(List.of(moduleNames));
|
||||
}
|
||||
|
||||
private void updateJavaFXDependencies() {
|
||||
clearJavaFXDependencies();
|
||||
|
||||
String configuration = getConfiguration();
|
||||
JavaFXModule.getJavaFXModules(this.modules).stream()
|
||||
.sorted()
|
||||
.forEach(javaFXModule -> {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
||||
} else {
|
||||
project.getDependencies().add(configuration,
|
||||
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
||||
getVersion(), getPlatform().getClassifier()));
|
||||
}
|
||||
});
|
||||
lastUpdatedConfiguration = configuration;
|
||||
}
|
||||
|
||||
private void clearJavaFXDependencies() {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getRepositories().remove(customSDKArtifactRepository);
|
||||
customSDKArtifactRepository = null;
|
||||
}
|
||||
|
||||
if (sdk != null && ! sdk.isEmpty()) {
|
||||
Map<String, String> dirs = new HashMap<>();
|
||||
dirs.put("name", "customSDKArtifactRepository");
|
||||
if (sdk.endsWith(File.separator)) {
|
||||
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
||||
} else {
|
||||
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
||||
}
|
||||
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
||||
}
|
||||
|
||||
if (lastUpdatedConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
||||
if (configuration != null) {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
||||
}
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* 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 HOLDER 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.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetector;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum JavaFXPlatform {
|
||||
|
||||
LINUX("linux", "linux-x86_64"),
|
||||
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
||||
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
||||
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
||||
WINDOWS("win", "windows-x86_64"),
|
||||
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
||||
OSX("mac", "osx-x86_64"),
|
||||
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
||||
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
||||
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
||||
|
||||
private final String classifier;
|
||||
private final String osDetectorClassifier;
|
||||
|
||||
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
||||
this.classifier = classifier;
|
||||
this.osDetectorClassifier = osDetectorClassifier;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public static JavaFXPlatform detect(Project project) {
|
||||
|
||||
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
||||
|
||||
if("true".equals(System.getProperty("java.awt.headless"))) {
|
||||
osClassifier += "-monocle";
|
||||
}
|
||||
|
||||
for ( JavaFXPlatform platform: values()) {
|
||||
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
String supportedPlatforms = Arrays.stream(values())
|
||||
.map(p->p.osDetectorClassifier)
|
||||
.collect(Collectors.joining("', '", "'", "'"));
|
||||
|
||||
throw new GradleException(
|
||||
String.format(
|
||||
"Unsupported JavaFX platform found: '%s'! " +
|
||||
"This plugin is designed to work on supported platforms only." +
|
||||
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* 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 HOLDER 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.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
|
||||
import org.openjfx.gradle.tasks.ExecTask;
|
||||
|
||||
public class JavaFXPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||
project.getPlugins().apply(ModuleSystemPlugin.class);
|
||||
|
||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||
|
||||
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 2021, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * 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.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* 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 HOLDER 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.
|
||||
*/
|
||||
package org.openjfx.gradle.tasks;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
||||
import org.openjfx.gradle.JavaFXModule;
|
||||
import org.openjfx.gradle.JavaFXOptions;
|
||||
import org.openjfx.gradle.JavaFXPlatform;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ExecTask extends DefaultTask {
|
||||
|
||||
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
||||
|
||||
private final Project project;
|
||||
private JavaExec execTask;
|
||||
|
||||
@Inject
|
||||
public ExecTask(Project project) {
|
||||
this.project = project;
|
||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
||||
if (execTask != null) {
|
||||
execTask.dependsOn(this);
|
||||
} else {
|
||||
throw new GradleException("Run task not found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void action() {
|
||||
if (execTask != null) {
|
||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
||||
|
||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
||||
|
||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||
);
|
||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||
|
||||
if (moduleOptions != null) {
|
||||
LOGGER.info("Modular JavaFX application found");
|
||||
// Remove empty JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
||||
} else {
|
||||
LOGGER.info("Non-modular JavaFX application found");
|
||||
// Remove all JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars);
|
||||
|
||||
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
||||
|
||||
var jvmArgs = new ArrayList<String>();
|
||||
jvmArgs.add("--add-modules");
|
||||
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
||||
|
||||
List<String> execJvmArgs = execTask.getJvmArgs();
|
||||
if (execJvmArgs != null) {
|
||||
jvmArgs.addAll(execJvmArgs);
|
||||
}
|
||||
jvmArgs.addAll(javaFXModuleJvmArgs);
|
||||
|
||||
execTask.setJvmArgs(jvmArgs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
||||
return jar.isFile() &&
|
||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.8.2"
|
||||
GIT_TAG="1.9.1"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: 6868b026fb...f8f50c0dd9
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Package: sparrow
|
||||
Version: 2.0.0-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
@@ -8,3 +8,4 @@ Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
SingleMainWindow=true
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.3</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -7,9 +7,7 @@ import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
@@ -27,10 +25,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
@@ -187,9 +182,6 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem sweepPrivateKey;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@@ -215,10 +207,14 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
private Storage.KeyDerivationService keyDerivationService;
|
||||
|
||||
private PauseTransition wait;
|
||||
|
||||
private Timeline statusTimeline;
|
||||
|
||||
private SearchWalletDialog searchWalletDialog;
|
||||
|
||||
private SendToManyDialog sendToManyDialog;
|
||||
|
||||
private DownloadVerifierDialog downloadVerifierDialog;
|
||||
@@ -280,6 +276,15 @@ public class AppController implements Initializable {
|
||||
void initializeView() {
|
||||
setPlatformApplicationMenu();
|
||||
|
||||
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
|
||||
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
|
||||
searchWalletDialog.close();
|
||||
}
|
||||
if(sendToManyDialog != null && sendToManyDialog.isShowing()) {
|
||||
sendToManyDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
rootStack.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||
event.acceptTransferModes(TransferMode.LINK);
|
||||
@@ -333,6 +338,8 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
||||
});
|
||||
|
||||
tabs.setPickOnBounds(false);
|
||||
|
||||
registerShortcuts();
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
@@ -379,7 +386,10 @@ public class AppController implements Initializable {
|
||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||
MenuItem homeItem = new MenuItem("Home Folder...");
|
||||
homeItem.setOnAction(this::restartInHome);
|
||||
restart.getItems().add(homeItem);
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.TESTNET4, Network.SIGNET));
|
||||
networks.remove(Network.get());
|
||||
for(Network network : networks) {
|
||||
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
||||
@@ -405,10 +415,6 @@ public class AppController implements Initializable {
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
@@ -971,19 +977,46 @@ public class AppController implements Initializable {
|
||||
AppServices.get().setPreventSleep(item.isSelected());
|
||||
}
|
||||
|
||||
public void restartInHome(ActionEvent event) {
|
||||
Args args = getRestartArgs();
|
||||
File initialDir = null;
|
||||
if(args.dir != null) {
|
||||
initialDir = new File(args.dir);
|
||||
}
|
||||
|
||||
Stage window = new Stage();
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Choose Sparrow Home Folder");
|
||||
directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getSparrowHome() : initialDir);
|
||||
File newHome = directoryChooser.showDialog(window);
|
||||
|
||||
if(newHome != null) {
|
||||
args.dir = newHome.getAbsolutePath();
|
||||
restart(event, args);
|
||||
}
|
||||
}
|
||||
|
||||
public void restart(ActionEvent event, Network network) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||
}
|
||||
|
||||
Args args = getRestartArgs();
|
||||
args.network = network;
|
||||
restart(event, args);
|
||||
}
|
||||
|
||||
private static Args getRestartArgs() {
|
||||
Args args = new Args();
|
||||
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
});
|
||||
|
||||
args.network = network;
|
||||
return args;
|
||||
}
|
||||
|
||||
private void restart(ActionEvent event, Args args) {
|
||||
try {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
@@ -1279,7 +1312,7 @@ public class AppController implements Initializable {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
@@ -1310,11 +1343,13 @@ public class AppController implements Initializable {
|
||||
if(key != null) {
|
||||
key.clear();
|
||||
}
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -1387,7 +1422,6 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@@ -1413,74 +1447,6 @@ public class AppController implements Initializable {
|
||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1573,13 +1539,46 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void searchWallets(List<WalletForm> walletForms) {
|
||||
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
if(searchWalletDialog != null) {
|
||||
if(!searchWalletDialog.getWalletForms().equals(walletForms)) {
|
||||
searchWalletDialog.close();
|
||||
} else {
|
||||
Stage stage = (Stage)searchWalletDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initModality(Modality.NONE);
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
}
|
||||
searchWalletDialog = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void showAllWalletsSummary(ActionEvent event) {
|
||||
List<List<WalletForm>> allWalletForms = new ArrayList<>();
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
allWalletForms.add(subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm())
|
||||
.filter(walletForm -> walletForm.getWallet().isValid() && !walletForm.isLocked()).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
if(allWalletForms.isEmpty() || allWalletForms.stream().allMatch(List::isEmpty)) {
|
||||
showErrorDialog("No wallets", "There are no open and unlocked wallets to summarize.");
|
||||
} else {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(allWalletForms);
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1591,7 +1590,7 @@ public class AppController implements Initializable {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
if(!walletForms.isEmpty()) {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms);
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(List.of(walletForms));
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
@@ -1647,14 +1646,6 @@ public class AppController implements Initializable {
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
tab.setOnCloseRequest(event -> {
|
||||
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.LEFT);
|
||||
@@ -1881,6 +1872,25 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||
//Add any missing previous outputs if available in open wallets
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).toList()) {
|
||||
TransactionOutPoint outpoint = psbtInput.getInput().getOutpoint();
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(outpoint.getHash());
|
||||
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > outpoint.getIndex()) {
|
||||
psbtInput.setNonWitnessUtxo(blockTransaction.getTransaction());
|
||||
ScriptType type = psbtInput.getScriptType();
|
||||
if(type != null && Arrays.asList(ScriptType.WITNESS_TYPES).contains(type)) {
|
||||
psbtInput.setWitnessUtxo(blockTransaction.getTransaction().getOutputs().get((int)outpoint.getIndex()));
|
||||
psbtInput.setNonWitnessUtxo(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
@@ -2148,7 +2158,7 @@ public class AppController implements Initializable {
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
@@ -2158,7 +2168,7 @@ public class AppController implements Initializable {
|
||||
deleteStorage(storage, true);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
@@ -2171,6 +2181,7 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -2473,7 +2484,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
@@ -2485,7 +2495,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2511,7 +2520,6 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2753,7 +2761,7 @@ public class AppController implements Initializable {
|
||||
public void disconnection(DisconnectionEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
|
||||
statusUpdated(new StatusEvent("Disconnected"));
|
||||
statusUpdated(new StatusEvent("Disconnected (click toggle on the right to connect)", 240));
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
@@ -3043,6 +3051,8 @@ public class AppController implements Initializable {
|
||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getFile() != null) {
|
||||
Optional<Tab> optExisting = tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData walletTabData && walletTabData.getStorage().getWalletFile().equals(event.getFile())).findFirst();
|
||||
optExisting.ifPresent(tab -> tabs.getTabs().remove(tab));
|
||||
openWalletFile(event.getFile(), true);
|
||||
} else {
|
||||
openWallet(true);
|
||||
|
||||
@@ -24,8 +24,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -95,11 +93,7 @@ public class AppServices {
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
private final InteractionServices interactionServices;
|
||||
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
@@ -117,6 +111,8 @@ public class AppServices {
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
private ElectrumServer.FeeRatesService feeRatesService;
|
||||
|
||||
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
||||
|
||||
private VersionCheckService versionCheckService;
|
||||
@@ -165,6 +161,11 @@ public class AppServices {
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
|
||||
if(httpClientService != null) {
|
||||
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
|
||||
shutdownService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -181,13 +182,12 @@ public class AppServices {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Config config = Config.get();
|
||||
connectionService = createConnectionService();
|
||||
feeRatesService = createFeeRatesService();
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
torService = createTorService();
|
||||
@@ -201,6 +201,8 @@ public class AppServices {
|
||||
} else {
|
||||
restartServices();
|
||||
}
|
||||
} else {
|
||||
EventManager.get().post(new DisconnectionEvent());
|
||||
}
|
||||
|
||||
addURIHandlers();
|
||||
@@ -284,8 +286,15 @@ public class AppServices {
|
||||
onlineProperty.setValue(true);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
if(connectionService.getValue() != null) {
|
||||
EventManager.get().post(connectionService.getValue());
|
||||
FeeRatesUpdatedEvent event = connectionService.getValue();
|
||||
if(event != null) {
|
||||
EventManager.get().post(event);
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
@@ -356,6 +365,15 @@ public class AppServices {
|
||||
return connectionService;
|
||||
}
|
||||
|
||||
private ElectrumServer.FeeRatesService createFeeRatesService() {
|
||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(feeRatesService.getValue());
|
||||
});
|
||||
|
||||
return feeRatesService;
|
||||
}
|
||||
|
||||
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||
@@ -476,7 +494,7 @@ public class AppServices {
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
if(config.isUseProxy() && config.getProxyServer() != null) {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
@@ -508,24 +526,15 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
|
||||
public static HttpClientService getHttpClientService() {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(httpClientService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
httpClientService = new HttpClientService(torProxy);
|
||||
} else {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
|
||||
httpClientService.setTorProxy(getTorProxy());
|
||||
}
|
||||
@@ -564,6 +573,34 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void runAfterDelay(long delay, Runnable runnable) {
|
||||
if(delay <= 0) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
} else {
|
||||
ScheduledService<Void> delayService = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
delayService.setOnSucceeded(_ -> {
|
||||
delayService.cancel();
|
||||
runnable.run();
|
||||
});
|
||||
delayService.setDelay(Duration.millis(delay));
|
||||
delayService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
@@ -898,6 +935,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setIconified(false);
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
@@ -1069,10 +1107,50 @@ public class AppServices {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
}
|
||||
|
||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
if(org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.UNIX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
||||
return waylandDisplay != null && !waylandDisplay.isEmpty();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
@@ -1103,17 +1181,18 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
if(event.getMempoolRateSizes() != null) {
|
||||
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();
|
||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||
feeRatesService = createFeeRatesService();
|
||||
feeRatesService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -89,28 +89,32 @@ public class SparrowDesktop extends Application {
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
final boolean showNewWallet = createNewWallet;
|
||||
//Delay opening new dialogs on Wayland
|
||||
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
|
||||
if(showNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
AppServices.get().start();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,9 +16,9 @@ import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.3";
|
||||
public static final String APP_VERSION = "2.0.0";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
@@ -66,6 +66,11 @@ public class SparrowWallet {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
@@ -79,7 +84,7 @@ public class SparrowWallet {
|
||||
|
||||
try {
|
||||
instance = new Instance(fileUriArguments);
|
||||
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
@@ -130,13 +135,13 @@ public class SparrowWallet {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public Instance(List<String> fileUriArguments) {
|
||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
super(SparrowWallet.APP_ID, true);
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
if(messageList != null) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
|
||||
@@ -5,17 +5,13 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
@@ -46,12 +42,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
|
||||
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +60,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
|
||||
@@ -45,6 +45,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
|
||||
@@ -77,7 +77,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
|
||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(addressCol);
|
||||
|
||||
@@ -31,6 +31,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
private CurrencyRate currencyRate;
|
||||
protected static final double STANDARD_WIDTH = 100.0;
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
@@ -143,4 +144,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
getSortOrder().add(column);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void setEqualPreferredColumnWidths() {
|
||||
for(TreeTableColumn<?, ?> column : getColumns()) {
|
||||
column.setPrefWidth(STANDARD_WIDTH);
|
||||
}
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
@@ -52,6 +53,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
setContextMenu(new ContextMenu());
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
|
||||
@@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
||||
super(ur, bbqr, false, false, selectBbqrButton);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
@@ -19,7 +20,7 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||
|
||||
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||
|
||||
private final Label signedBy;
|
||||
@@ -100,7 +101,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
|
||||
|
||||
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
|
||||
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null);
|
||||
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", manifestDisabled);
|
||||
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
||||
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
||||
|
||||
@@ -154,6 +155,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
release.set(null);
|
||||
signedBy.setText("");
|
||||
signedBy.setGraphic(null);
|
||||
signedBy.setTooltip(null);
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseVerified.setText("");
|
||||
@@ -263,6 +265,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private void verify() {
|
||||
manifestDisabled.set(false);
|
||||
publicKeyDisabled.set(false);
|
||||
|
||||
if(signature.get() == null || manifest.get() == null) {
|
||||
@@ -283,12 +286,14 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
||||
signedBy.setText(message);
|
||||
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
||||
signedBy.setTooltip(new Tooltip(result.fingerprint()));
|
||||
|
||||
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
||||
publicKeyDisabled.set(true);
|
||||
}
|
||||
|
||||
if(manifest.get().equals(release.get())) {
|
||||
manifestDisabled.set(true);
|
||||
releaseHash.setText("No hash required, signature signs release file directly");
|
||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
@@ -303,6 +308,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Throwable e = event.getSource().getException();
|
||||
signedBy.setText(getDisplayMessage(e));
|
||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
signedBy.setTooltip(null);
|
||||
clearReleaseFields();
|
||||
});
|
||||
|
||||
|
||||
@@ -585,12 +585,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
getItems().add(createCpfp);
|
||||
}
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
if(!Config.get().isBlockExplorerDisabled()) {
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
}
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
@@ -604,7 +606,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
protected static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
@@ -612,12 +614,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
hide();
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
if(!Config.get().isBlockExplorerDisabled()) {
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
}
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
@@ -626,6 +632,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(date);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyDate);
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
@@ -634,6 +641,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(blockTransaction.getHashAsString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyTxid);
|
||||
|
||||
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
||||
copyHeight.setOnAction(AE -> {
|
||||
@@ -642,8 +650,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
|
||||
getItems().add(copyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,7 +841,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||
cell.getStyleClass().add("unconfirmed-row");
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
|
||||
cell.getStyleClass().add("summary-row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.stream.Collectors;
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
@@ -45,6 +47,18 @@ public class FeeRangeSlider extends Slider {
|
||||
updateMaxFeeRange(newValue.doubleValue());
|
||||
}
|
||||
});
|
||||
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
|
||||
@@ -240,6 +240,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
javafx.application.Platform.runLater(passwordField::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
}
|
||||
|
||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
|
||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + KeyDerivation.writePath(keystore.getKeyDerivation().getDerivation()) + ".");
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
|
||||
@@ -171,11 +171,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
} else if(exporter instanceof Bip129) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -38,6 +39,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private final KeystoreFileImport importer;
|
||||
private String fileName;
|
||||
private byte[] fileBytes;
|
||||
private String password;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
@@ -46,6 +48,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
@@ -83,7 +86,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
@@ -151,6 +154,8 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -32,17 +35,21 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||
|
||||
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
|
||||
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
@@ -104,7 +111,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new MessageSignDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
@@ -199,13 +207,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||
showQrButton.setDisable(wallet == null);
|
||||
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
showQrButton.setGraphicTextGap(5);
|
||||
showQrButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(!canSign);
|
||||
@@ -267,7 +269,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
@@ -472,7 +474,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
@@ -495,6 +497,82 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
|
||||
joiner.add(walletNode.getWallet().getScriptType().toString());
|
||||
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Text File");
|
||||
fileChooser.setInitialFileName("signmessage.txt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
|
||||
file = new File(file.getAbsolutePath() + ".txt");
|
||||
}
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
|
||||
writer.write(joiner.toString());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving signing message", e);
|
||||
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed Text File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
if(matcher.matches()) {
|
||||
String signedMessage = matcher.group(1);
|
||||
String signedAddress = matcher.group(2);
|
||||
String signedSignature = matcher.group(3);
|
||||
|
||||
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
|
||||
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
|
||||
return;
|
||||
} else if(!signedAddress.trim().equals(address.getText().trim())) {
|
||||
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
message.setText(signedMessage);
|
||||
signature.setText(signedSignature);
|
||||
} else {
|
||||
signature.setText(content);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error loading signed message", e);
|
||||
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Glyph getSignGlyph() {
|
||||
if(wallet != null) {
|
||||
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
@@ -539,4 +617,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageSignDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
SplitMenuButton signByButton = new SplitMenuButton();
|
||||
signByButton.setText("Sign by QR");
|
||||
signByButton.setDisable(wallet == null);
|
||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
signByButton.setGraphicTextGap(5);
|
||||
signByButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
||||
exportFile.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
||||
importFile.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
signByButton.getItems().addAll(exportFile, importFile);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(signByButton, buttonData);
|
||||
|
||||
return signByButton;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -41,174 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason != MixFailReason.CANCEL) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||
tt.setOnShowing(event -> {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
|
||||
registeredInputsService.setOnSucceeded(eventStateHandler -> {
|
||||
if(registeredInputsService.getValue() != null) {
|
||||
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
|
||||
}
|
||||
});
|
||||
registeredInputsService.start();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
@@ -15,10 +16,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
private final DeterministicSeed.Type type;
|
||||
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
this.type = keystore.getSeed().getType();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
@@ -29,7 +33,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -43,7 +47,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -57,4 +61,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.util.Optional;
|
||||
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
@@ -43,10 +44,11 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private Button confirmButton;
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
@@ -59,7 +61,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation(), false);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
@@ -77,6 +79,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
protected void enterMnemonic(int numWords) {
|
||||
generatedMnemonicCode = null;
|
||||
super.enterMnemonic(numWords);
|
||||
@@ -243,7 +249,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
@@ -111,23 +113,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
} else {
|
||||
ScheduledService<Void> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setDelay(Duration.millis(500));
|
||||
service.setOnSucceeded(event1 -> {
|
||||
service.cancel();
|
||||
AppServices.runAfterDelay(500, () -> {
|
||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||
});
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +141,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
showWordList(words);
|
||||
}
|
||||
|
||||
protected void showWordList(List<String> words) {
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
@@ -175,7 +167,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -189,7 +181,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -215,7 +207,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
@@ -224,7 +216,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
@@ -242,7 +234,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
if(!getWordlistProvider().isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
@@ -278,13 +270,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
|
||||
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -302,7 +301,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
for(int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
if(entry.nextField != null) {
|
||||
if(i == words.length - 2 && isValid(word)) {
|
||||
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
|
||||
label.requestFocus();
|
||||
} else {
|
||||
entry.nextField.requestFocus();
|
||||
@@ -335,8 +334,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
@@ -357,7 +355,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -378,28 +376,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
private final WordlistProvider wordlistProvider;
|
||||
private final int wordNumber;
|
||||
private final ObservableList<String> wordEntryList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordList = wordList;
|
||||
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordlistProvider = wordlistProvider;
|
||||
this.wordNumber = wordNumber;
|
||||
this.wordEntryList = wordEntryList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
try {
|
||||
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||
}
|
||||
@@ -412,7 +406,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
for(String word : wordlistProvider.getWordlist()) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
@@ -424,7 +418,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean allPreviousWordsValid() {
|
||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -485,17 +479,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
protected interface WordlistProvider {
|
||||
List<String> getWordlist();
|
||||
boolean isValid(String word);
|
||||
boolean supportsPossibleLastWords();
|
||||
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
private static class Bip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
||||
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Slip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Slip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
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.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
|
||||
import com.sparrowwallet.sparrow.io.Slip39;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicShareImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final List<List<String>> mnemonicShares = new ArrayList<>();
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private int currentShare;
|
||||
|
||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 20 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(20);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
resetShares();
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {20, 33};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
resetShares();
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
|
||||
calculateButton.setVisible(false);
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
lastShare();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Display the last share added"));
|
||||
backButton.setVisible(currentShare > 0);
|
||||
|
||||
nextButton = new Button("Next");
|
||||
nextButton.setOnAction(event -> {
|
||||
nextShare();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Add the next share"));
|
||||
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
|
||||
nextButton.setDefaultButton(true);
|
||||
nextButton.setDisable(true);
|
||||
|
||||
return List.of(backButton, nextButton, calculateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter existing share");
|
||||
}
|
||||
|
||||
private void resetShares() {
|
||||
currentShare = 0;
|
||||
mnemonicShares.clear();
|
||||
}
|
||||
|
||||
private void lastShare() {
|
||||
currentShare--;
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
}
|
||||
|
||||
private void nextShare() {
|
||||
if(currentShare == mnemonicShares.size()) {
|
||||
mnemonicShares.add(wordEntriesProperty.get());
|
||||
} else {
|
||||
mnemonicShares.set(currentShare, wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
currentShare++;
|
||||
|
||||
if(currentShare < mnemonicShares.size()) {
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
} else {
|
||||
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
|
||||
}
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
boolean validSet = false;
|
||||
boolean complete = false;
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
|
||||
validChecksum = true;
|
||||
|
||||
List<List<String>> existing = new ArrayList<>(mnemonicShares);
|
||||
if(currentShare >= mnemonicShares.size()) {
|
||||
existing.add(wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
validSet = true;
|
||||
complete = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(Slip39.Slip39ProgressException e) {
|
||||
validSet = true;
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException mnemonicException) {
|
||||
invalidLabel.setText(mnemonicException.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
|
||||
} else {
|
||||
invalidLabel.setText("Import Error");
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setVisible(complete);
|
||||
backButton.setVisible(currentShare > 0 && !complete);
|
||||
nextButton.setDisable(!validChecksum || !validSet);
|
||||
validLabel.setVisible(complete);
|
||||
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
|
||||
invalidLabel.setVisible(!complete && !empty);
|
||||
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation, false);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void prepareImport() {
|
||||
nextShare();
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
|
||||
setExpanded(true);
|
||||
enterMnemonicButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
} catch (ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation, false);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public static Glyph getIncompleteGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
warningGlyph.setFontSize(12);
|
||||
return warningGlyph;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.SLIP39);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
@@ -128,7 +124,7 @@ public class PayNymAvatar extends StackPane {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
|
||||
@@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
|
||||
@@ -407,6 +407,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
} else {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds (" + total + " sats) for a transaction at this fee rate." +
|
||||
"\n\nContinue with a minimum fee rate transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
@@ -42,7 +43,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
private static final double ANIMATION_PERIOD_MILLIS = 200d;
|
||||
|
||||
private static final int DEFAULT_QR_SIZE = 580;
|
||||
private static final int REDUCED_QR_SIZE = 520;
|
||||
@@ -100,6 +101,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
qrImageView = new ImageView();
|
||||
stackPane.getChildren().add(qrImageView);
|
||||
|
||||
qrImageView.setOnScroll(scrollEvent -> {
|
||||
if(animateQRService != null && animateQRService.isRunning() && scrollEvent.getDeltaY() != 0) {
|
||||
Duration duration = animateQRService.getPeriod();
|
||||
Duration newDuration = scrollEvent.getDeltaY() > 0 ? duration.multiply(1.1) : duration.multiply(0.9);
|
||||
if(newDuration.lessThan(Duration.millis(ANIMATION_PERIOD_MILLIS*10)) && newDuration.greaterThan(Duration.millis(ANIMATION_PERIOD_MILLIS/2))) {
|
||||
animateQRService.setPeriod(newDuration);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
nextPart();
|
||||
@@ -159,6 +170,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
if(qrImageView.getImage() == null) {
|
||||
Label warning = new Label("Message is too long for display as a QR code");
|
||||
stackPane.getChildren().add(warning);
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
@@ -249,6 +265,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUseBbqrEncoding() {
|
||||
return useBbqrEncoding;
|
||||
}
|
||||
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
||||
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -621,7 +622,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
||||
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
|
||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
||||
return new KeyDerivation(fingerprint, KeyDerivation.writePath(path));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -54,10 +54,10 @@ public class ScriptArea extends CodeArea {
|
||||
ScriptChunk chunk = script.getChunks().get(i);
|
||||
if(chunk.isOpCode()) {
|
||||
append(chunk.toString(), "script-opcode");
|
||||
} else if(chunk.isSignature()) {
|
||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isSignature()) {
|
||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isTaprootControlBlock()) {
|
||||
append("<controlblock>", "script-controlblock");
|
||||
} else if(chunk.isString()) {
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,10 +29,11 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
@@ -48,7 +58,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet");
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -79,8 +89,9 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setEqualPreferredColumnWidths();
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
results.setEditable(true);
|
||||
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
@@ -117,7 +128,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
labelCol.setCellFactory(p -> new LabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
@@ -130,12 +141,20 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType exportButtonType = new ButtonType("Export CSV", ButtonBar.ButtonData.LEFT);
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
dialogPane.getButtonTypes().addAll(exportButtonType, cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
Button exportButton = (Button)dialogPane.lookupButton(exportButtonType);
|
||||
exportButton.setGraphic(GlyphUtils.getDownArrowGlyph());
|
||||
exportButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
exportResults(showWallet);
|
||||
});
|
||||
|
||||
Button showButton = (Button)dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
@@ -147,7 +166,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallets(newValue.toLowerCase(Locale.ROOT));
|
||||
searchWallets(newValue);
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
@@ -157,42 +176,41 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallets(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
public List<WalletForm> getWalletForms() {
|
||||
return walletForms;
|
||||
}
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
private void searchWallets(String searchPhrase) {
|
||||
Set<Entry> matchingEntries = new LinkedHashSet<>();
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!searchPhrase.isEmpty()) {
|
||||
Set<String> searchWords = new LinkedHashSet<>(Arrays.stream(searchPhrase.split("\\s+"))
|
||||
.filter(text -> isAddress(text) || isHash(text) || isHashIndex(text)).toList());
|
||||
String freeText = removeOccurrences(searchPhrase, searchWords).trim();
|
||||
if(!freeText.isEmpty()) {
|
||||
searchWords.add(freeText);
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
for(String searchText : searchWords) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
searchText = searchText.toLowerCase(Locale.ROOT);
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
@@ -203,22 +221,38 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
hashIndexEntry.getHashIndex().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), new ArrayList<>(matchingEntries));
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
@@ -239,6 +273,95 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAddress(String text) {
|
||||
try {
|
||||
Address.fromString(text);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHash(String text) {
|
||||
return text.length() == 64 && Utils.isHex(text);
|
||||
}
|
||||
|
||||
private boolean isHashIndex(String text) {
|
||||
String[] parts = text.split(":");
|
||||
if(parts.length == 2 && isHash(parts[0])) {
|
||||
try {
|
||||
Integer.parseInt(parts[1]);
|
||||
return true;
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String removeOccurrences(String inputString, Collection<String> stringsToRemove) {
|
||||
for(String str : stringsToRemove) {
|
||||
inputString = inputString.replaceAll("(?i)" + str, "");
|
||||
}
|
||||
|
||||
return inputString;
|
||||
}
|
||||
|
||||
public void exportResults(boolean showWallet) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export search results to CSV");
|
||||
fileChooser.setInitialFileName(getDialogPane().getHeaderText() + ".csv");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
List<String> headers = new ArrayList<>(List.of("Wallet", "Account", "Type", "Date", "Txid / Address / Output", "Label", "Value"));
|
||||
if(!showWallet) {
|
||||
headers.remove(0);
|
||||
}
|
||||
writer.writeRecord(headers.toArray(new String[0]));
|
||||
for(TreeItem<Entry> item : results.getRoot().getChildren()) {
|
||||
Entry entry = item.getValue();
|
||||
if(showWallet) {
|
||||
writer.write(entry.getWallet().getMasterName());
|
||||
}
|
||||
writer.write(entry.getWallet().getDisplayName());
|
||||
writer.write(entry.getEntryType());
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
writer.write(transactionEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()));
|
||||
writer.write(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
writer.write("");
|
||||
writer.write(nodeEntry.getAddress().toString());
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
writer.write(hashIndexEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(hashIndexEntry.getBlockTransaction().getDate()));
|
||||
writer.write(hashIndexEntry.getHashIndex().toString());
|
||||
} else {
|
||||
writer.write("");
|
||||
writer.write("");
|
||||
}
|
||||
writer.write(entry.getLabel());
|
||||
writer.write(getCoinValue(entry.getValue() == null ? 0 : entry.getValue()));
|
||||
writer.endRecord();
|
||||
}
|
||||
writer.close();
|
||||
} catch(IOException e) {
|
||||
log.error("Error exporting search results as CSV", e);
|
||||
AppServices.showErrorDialog("Error exporting search results as CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getCoinValue(Long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return BitcoinUnit.BTC.equals(results.getBitcoinUnit()) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
@@ -264,15 +387,19 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
ContextMenu copyMenu;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
copyMenu = new TransactionContextMenu(getText(), transactionEntry.getBlockTransaction());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
copyMenu = new AddressContextMenu(nodeEntry.getAddress(), nodeEntry.getOutputDescriptor(), null, false, null);
|
||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||
copyMenu = new HashIndexEntryContextMenu(null, utxoEntry);
|
||||
} else {
|
||||
copyMenu = new ContextMenu();
|
||||
}
|
||||
copyMenu.getItems().removeIf(menuItem -> !menuItem.getText().startsWith("Copy"));
|
||||
setContextMenu(copyMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class SeedDisplayDialog extends Dialog<Void> {
|
||||
public SeedDisplayDialog(Keystore decryptedKeystore) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
|
||||
int lines = Math.ceilDiv(decryptedKeystore.getSeed().getMnemonicCode().size(), 3);
|
||||
int height = lines * 40;
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
@@ -39,8 +44,19 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) {
|
||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(seedQRButtonType);
|
||||
|
||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
showSeedQR(decryptedKeystore);
|
||||
});
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(150 + height);
|
||||
@@ -48,4 +64,15 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
|
||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||
}
|
||||
|
||||
private void showSeedQR(Keystore decryptedKeystore) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Sensitive QR", "The following QR contains these seed words. " +
|
||||
"Be careful before displaying or digitally recording it.\n\nAre you sure you want to continue?", ButtonType.YES, ButtonType.NO);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
String seedQR = SeedQR.getSeedQR(decryptedKeystore.getSeed());
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(seedQR);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
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;
|
||||
|
||||
public class SelectableCodeArea extends CodeArea {
|
||||
public SelectableCodeArea() {
|
||||
super();
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setDisable(true);
|
||||
copy.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getSelectedText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
MenuItem copyAll = new MenuItem("Copy All");
|
||||
copyAll.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
contextMenu.getItems().addAll(copy, copyAll);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
selectedTextProperty().addListener((observable, oldValue, newValue) -> {
|
||||
copy.setDisable(newValue.isEmpty());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,9 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -246,19 +244,9 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
@@ -339,11 +327,9 @@ public class TransactionDiagram extends GridPane {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||
if(externalUserSet) {
|
||||
Glyph bracketGlyph = walletTx.isCoinControlUsed() ? getLockGlyph() : getCoinsGlyph();
|
||||
String tooltipText = walletTx.getWallet().getFullDisplayName();
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
@@ -474,14 +460,6 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
@@ -570,7 +548,7 @@ public class TransactionDiagram extends GridPane {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
@@ -952,46 +930,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
private Glyph getCoinsGlyph() {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
@@ -1094,20 +1036,6 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final boolean required;
|
||||
|
||||
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mix Partner" + (required ? "" : "?");
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ public class UsbStatusButton extends MenuButton {
|
||||
public void setDevices(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_3 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
toggleItem.setOnAction(event -> {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
|
||||
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
@@ -19,9 +19,10 @@ import javafx.scene.shape.Circle;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
@@ -137,7 +138,7 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
private void addWalletIcon(String walletId) {
|
||||
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
@@ -176,6 +177,8 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletIconStreamHandler.class);
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URLConnection(url) {
|
||||
@@ -186,17 +189,19 @@ public class WalletIcon extends StackPane {
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
String walletId = url.getPath();
|
||||
String walletId = url.getPath().replaceAll("%20", " ").replaceAll("%23", "#");
|
||||
String query = url.getQuery();
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet == null) {
|
||||
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||
log.warn("Cannot find wallet for wallet id " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
if(query.startsWith(QUERY)) {
|
||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||
throw new IOException("No icon data for " + walletId);
|
||||
log.warn("No icon data for " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||
@@ -207,7 +212,12 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
throw new MalformedURLException("Cannot load url " + url);
|
||||
log.warn("Cannot load url " + url);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
private static InputStream getFallbackIconStream() {
|
||||
return WalletIconStreamHandler.class.getResourceAsStream("/image/sparrow.png");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(),
|
||||
new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault(), new Samourai());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
@@ -59,7 +60,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig()));
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(),
|
||||
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig(), new PassportMultisig()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
|
||||
@@ -18,17 +18,21 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletSummaryDialog extends Dialog<Void> {
|
||||
public WalletSummaryDialog(List<WalletForm> walletForms) {
|
||||
if(walletForms.isEmpty()) {
|
||||
public WalletSummaryDialog(List<List<WalletForm>> summaryWalletFormsList) {
|
||||
List<List<WalletForm>> walletFormsList = new ArrayList<>(summaryWalletFormsList);
|
||||
walletFormsList.removeIf(List::isEmpty);
|
||||
if(walletFormsList.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to summarize");
|
||||
}
|
||||
|
||||
Wallet masterWallet = walletForms.get(0).getMasterWallet();
|
||||
boolean allOpenWallets = walletFormsList.size() > 1;
|
||||
List<Wallet> masterWallets = walletFormsList.stream().map(walletForms -> walletForms.get(0).getMasterWallet()).toList();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
@@ -37,7 +41,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/transactions.css").toExternalForm());
|
||||
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + masterWallet.getName());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -64,7 +68,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
});
|
||||
balanceColumn.setCellFactory(p -> new CoinCell());
|
||||
table.getColumns().add(balanceColumn);
|
||||
table.setUnitFormat(masterWallet);
|
||||
table.setUnitFormat(masterWallets.get(0));
|
||||
|
||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
||||
@@ -77,19 +81,27 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
table.setCurrencyRate(currencyRate);
|
||||
}
|
||||
|
||||
SummaryEntry rootEntry = new SummaryEntry(walletForms);
|
||||
Entry rootEntry = allOpenWallets ? new AllSummaryEntry(walletFormsList) : new SummaryEntry(walletFormsList.get(0));
|
||||
TreeItem<Entry> rootItem = new TreeItem<>(rootEntry);
|
||||
for(Entry childEntry : rootEntry.getChildren()) {
|
||||
TreeItem<Entry> childItem = new TreeItem<>(childEntry);
|
||||
rootItem.getChildren().add(childItem);
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
if(allOpenWallets) {
|
||||
for(Entry walletEntry : childEntry.getChildren()) {
|
||||
TreeItem<Entry> walletItem = new TreeItem<>(walletEntry);
|
||||
childItem.getChildren().add(walletItem);
|
||||
walletItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)walletEntry)));
|
||||
}
|
||||
} else {
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
}
|
||||
}
|
||||
|
||||
table.setShowRoot(true);
|
||||
table.setRoot(rootItem);
|
||||
rootItem.setExpanded(true);
|
||||
|
||||
table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
table.setEqualPreferredColumnWidths();
|
||||
table.setPrefWidth(450);
|
||||
|
||||
VBox vBox = new VBox();
|
||||
@@ -97,6 +109,14 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
|
||||
hBox.getChildren().add(vBox);
|
||||
|
||||
Wallet balanceWallet;
|
||||
if(allOpenWallets) {
|
||||
balanceWallet = new Wallet();
|
||||
balanceWallet.getChildWallets().addAll(masterWallets.stream().flatMap(mws -> mws.getAllWallets().stream()).toList());
|
||||
} else {
|
||||
balanceWallet = masterWallets.get(0);
|
||||
}
|
||||
|
||||
NumberAxis xAxis = new NumberAxis();
|
||||
xAxis.setSide(Side.BOTTOM);
|
||||
xAxis.setForceZeroInRange(false);
|
||||
@@ -104,7 +124,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
yAxis.setSide(Side.LEFT);
|
||||
BalanceChart balanceChart = new BalanceChart(xAxis, yAxis);
|
||||
balanceChart.initialize(new WalletTransactionsEntry(masterWallet, true));
|
||||
balanceChart.initialize(new WalletTransactionsEntry(balanceWallet, true));
|
||||
balanceChart.setAnimated(false);
|
||||
balanceChart.setLegendVisible(false);
|
||||
balanceChart.setVerticalGridLinesVisible(false);
|
||||
@@ -120,6 +140,32 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public static class AllSummaryEntry extends Entry {
|
||||
private AllSummaryEntry(List<List<WalletForm>> walletFormsList) {
|
||||
super(null, "All Wallets", walletFormsList.stream().map(SummaryEntry::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
long value = 0;
|
||||
for(Entry entry : getChildren()) {
|
||||
value += entry.getValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return Function.TRANSACTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SummaryEntry extends Entry {
|
||||
private SummaryEntry(List<WalletForm> walletForms) {
|
||||
super(walletForms.get(0).getWallet(), walletForms.get(0).getWallet().getName(), walletForms.stream().map(WalletForm::getWalletTransactionsEntry).collect(Collectors.toList()));
|
||||
|
||||
@@ -28,16 +28,18 @@ import java.util.List;
|
||||
public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
protected final Wallet wallet;
|
||||
protected final KeystoreXprvImport importer;
|
||||
protected final KeyDerivation defaultDerivation;
|
||||
|
||||
private Button enterXprvButton;
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private ExtendedKey xprv;
|
||||
|
||||
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer) {
|
||||
public XprvKeystoreImportPane(Wallet wallet, KeystoreXprvImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Extended key import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
@@ -47,6 +49,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
super("Master Private Key", "BIP32 key", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
this.wallet = null;
|
||||
this.importer = null;
|
||||
this.defaultDerivation = keystore.getKeyDerivation();
|
||||
|
||||
try {
|
||||
this.xprv = keystore.getExtendedMasterPrivateKey();
|
||||
@@ -79,7 +82,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation());
|
||||
importKeystore(getDefaultDerivation());
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
@@ -97,6 +100,10 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void enterXprv() {
|
||||
setDescription("Enter master private key");
|
||||
showHideLink.setVisible(false);
|
||||
@@ -149,7 +156,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setVisible(true);
|
||||
setDescription("Ready to import");
|
||||
xprv = ExtendedKey.fromDescriptor(xprvField.getText());
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
});
|
||||
|
||||
xprvField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
@@ -17,7 +16,6 @@ public class SpendUtxoEvent {
|
||||
private final Long fee;
|
||||
private final boolean requireAllUtxos;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
@@ -32,19 +30,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = requireAllUtxos;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
@@ -56,7 +41,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = null;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
@@ -88,10 +72,6 @@ public class SpendUtxoEvent {
|
||||
return replacedTransaction;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolIndexHighFrequencyEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WhirlpoolIndexHighFrequencyEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolMixEvent {
|
||||
private final Wallet wallet;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
private final MixProgress mixProgress;
|
||||
private final Utxo nextUtxo;
|
||||
private final MixFailReason mixFailReason;
|
||||
private final String mixError;
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextUtxo() {
|
||||
return nextUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
|
||||
super(wallet, utxo, nextUtxo);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
FILE_EXPORT('\uf56e'),
|
||||
FILE_PDF('\uf1c1'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
|
||||
@@ -183,6 +183,13 @@ public class GlyphUtils {
|
||||
return successGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWarningGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
@@ -203,4 +210,11 @@ public class GlyphUtils {
|
||||
busyGlyph.setFontSize(12);
|
||||
return busyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDownArrowGlyph() {
|
||||
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
downGlyph.getStyleClass().add("arrow-down");
|
||||
downGlyph.setFontSize(12);
|
||||
return downGlyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,535 +1,270 @@
|
||||
/**
|
||||
* Copyright 2019 Pratanu Mandal
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.sparrowwallet.sparrow.instance;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.UnixDomainSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The <code>Instance</code> class is the primary logical entry point to the library.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // unique application ID
|
||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
||||
*
|
||||
* // create Instance instance
|
||||
* Instance unique = new Instance(APP_ID) {
|
||||
* @Override
|
||||
* protected void receiveMessage(String message) {
|
||||
* // print received message (timestamp)
|
||||
* System.out.println(message);
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected String sendMessage() {
|
||||
* // send timestamp as message
|
||||
* Timestamp ts = new Timestamp(new Date().getTime());
|
||||
* return "Another instance launch attempted: " + ts.toString();
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.3
|
||||
*
|
||||
*/
|
||||
public abstract class Instance {
|
||||
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
||||
|
||||
// starting position of port check
|
||||
private static final int PORT_START = 7221;
|
||||
|
||||
// system temporary directory path
|
||||
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
|
||||
|
||||
/**
|
||||
* Unique string representing the application ID.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*/
|
||||
public final String APP_ID;
|
||||
|
||||
// auto exit from application or not
|
||||
private final boolean AUTO_EXIT;
|
||||
|
||||
// lock server port
|
||||
private int port;
|
||||
|
||||
// lock server socket
|
||||
private ServerSocket server;
|
||||
|
||||
// lock file RAF object
|
||||
private RandomAccessFile lockRAF;
|
||||
|
||||
// file lock for the lock file RAF object
|
||||
private FileLock fileLock;
|
||||
private static final String LINK_ENV_PROPERTY = "SPARROW_NO_LOCK_FILE_LINK";
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
*/
|
||||
public Instance(final String APP_ID) {
|
||||
this(APP_ID, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
||||
*/
|
||||
public Instance(final String APP_ID, final boolean AUTO_EXIT) {
|
||||
this.APP_ID = APP_ID;
|
||||
this.AUTO_EXIT = AUTO_EXIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain lock. If not possible, send data to first instance.
|
||||
*
|
||||
* @deprecated Use <code>acquireLock()</code> instead.
|
||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||
*/
|
||||
@Deprecated
|
||||
public void lock() throws InstanceException {
|
||||
acquireLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain lock. If not possible, send data to first instance.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @return true if able to acquire lock, false otherwise
|
||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||
*/
|
||||
public boolean acquireLock() throws InstanceException {
|
||||
// try to obtain port number from lock file
|
||||
port = lockFile();
|
||||
|
||||
if (port == -1) {
|
||||
// failed to fetch port number
|
||||
// try to start server
|
||||
startServer();
|
||||
}
|
||||
else {
|
||||
// port number fetched from lock file
|
||||
// try to start client
|
||||
doClient();
|
||||
}
|
||||
|
||||
return (server != null);
|
||||
}
|
||||
|
||||
// start the server
|
||||
private void startServer() throws InstanceException {
|
||||
// try to create server
|
||||
port = PORT_START;
|
||||
while (true) {
|
||||
try {
|
||||
server = new ServerSocket(port, 50, InetAddress.getByName(null));
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
port++;
|
||||
}
|
||||
}
|
||||
|
||||
// try to lock file
|
||||
lockFile(port);
|
||||
|
||||
// server created successfully; this is the first instance
|
||||
// keep listening for data from other instances
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!server.isClosed()) {
|
||||
try {
|
||||
// establish connection
|
||||
final Socket socket = server.accept();
|
||||
|
||||
// handle socket on a different thread to allow parallel connections
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// open writer
|
||||
OutputStream os = socket.getOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(os);
|
||||
|
||||
// open reader
|
||||
InputStream is = socket.getInputStream();
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
|
||||
// read message length from client
|
||||
int length = dis.readInt();
|
||||
|
||||
// read message string from client
|
||||
String message = null;
|
||||
if (length > -1) {
|
||||
byte[] messageBytes = new byte[length];
|
||||
int bytesRead = dis.read(messageBytes, 0, length);
|
||||
message = new String(messageBytes, 0, bytesRead, "UTF-8");
|
||||
}
|
||||
|
||||
// write response to client
|
||||
if (APP_ID == null) {
|
||||
dos.writeInt(-1);
|
||||
}
|
||||
else {
|
||||
byte[] appId = APP_ID.getBytes("UTF-8");
|
||||
|
||||
dos.writeInt(appId.length);
|
||||
dos.write(appId);
|
||||
}
|
||||
dos.flush();
|
||||
|
||||
// close writer and reader
|
||||
dos.close();
|
||||
dis.close();
|
||||
|
||||
// perform user action on message
|
||||
receiveMessage(message);
|
||||
|
||||
// close socket
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// start socket thread
|
||||
thread.start();
|
||||
} catch (SocketException e) {
|
||||
if (!server.isClosed()) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
thread.start();
|
||||
}
|
||||
|
||||
// do client tasks
|
||||
private void doClient() throws InstanceException {
|
||||
// get localhost address
|
||||
InetAddress address = null;
|
||||
try {
|
||||
address = InetAddress.getByName(null);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
|
||||
// try to establish connection to server
|
||||
Socket socket = null;
|
||||
try {
|
||||
socket = new Socket(address, port);
|
||||
} catch (IOException e) {
|
||||
// connection failed try to start server
|
||||
startServer();
|
||||
}
|
||||
|
||||
// connection successful try to connect to server
|
||||
if (socket != null) {
|
||||
try {
|
||||
// get message to be sent to first instance
|
||||
String message = sendMessage();
|
||||
|
||||
// open writer
|
||||
OutputStream os = socket.getOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(os);
|
||||
|
||||
// open reader
|
||||
InputStream is = socket.getInputStream();
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
|
||||
// write message to server
|
||||
if (message == null) {
|
||||
dos.writeInt(-1);
|
||||
}
|
||||
else {
|
||||
byte[] messageBytes = message.getBytes("UTF-8");
|
||||
|
||||
dos.writeInt(messageBytes.length);
|
||||
dos.write(messageBytes);
|
||||
}
|
||||
|
||||
dos.flush();
|
||||
|
||||
// read response length from server
|
||||
int length = dis.readInt();
|
||||
|
||||
// read response string from server
|
||||
String response = null;
|
||||
if (length > -1) {
|
||||
byte[] responseBytes = new byte[length];
|
||||
int bytesRead = dis.read(responseBytes, 0, length);
|
||||
response = new String(responseBytes, 0, bytesRead, "UTF-8");
|
||||
}
|
||||
|
||||
// close writer and reader
|
||||
dos.close();
|
||||
dis.close();
|
||||
|
||||
if (response.equals(APP_ID)) {
|
||||
// validation successful
|
||||
if (AUTO_EXIT) {
|
||||
// perform pre-exit tasks
|
||||
beforeExit();
|
||||
// exit this instance
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// validation failed, this is the first instance
|
||||
startServer();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} finally {
|
||||
// close socket
|
||||
try {
|
||||
if (socket != null) socket.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to get port from lock file
|
||||
private int lockFile() throws InstanceException {
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to get port from lock file
|
||||
if (file.exists()) {
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
|
||||
return Integer.parseInt(br.readLine());
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} catch (NumberFormatException e) {
|
||||
// do nothing
|
||||
} finally {
|
||||
try {
|
||||
if (br != null) br.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// try to write port to lock file
|
||||
private void lockFile(int port) throws InstanceException {
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to write port to lock file
|
||||
BufferedWriter bw = null;
|
||||
try {
|
||||
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
|
||||
bw.write(String.valueOf(port));
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} finally {
|
||||
try {
|
||||
if (bw != null) bw.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// try to obtain file lock
|
||||
try {
|
||||
lockRAF = new RandomAccessFile(file, "rw");
|
||||
FileChannel fc = lockRAF.getChannel();
|
||||
fileLock = fc.tryLock(0, Long.MAX_VALUE, true);
|
||||
if (fileLock == null) {
|
||||
throw new InstanceException("Failed to obtain file lock");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new InstanceException(e);
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
* @deprecated Use <code>freeLock()</code> instead.
|
||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||
*/
|
||||
@Deprecated
|
||||
public void free() throws InstanceException {
|
||||
freeLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @return true if able to release lock, false otherwise
|
||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||
*/
|
||||
public boolean freeLock() throws InstanceException {
|
||||
try {
|
||||
// close server socket
|
||||
if (server != null) {
|
||||
server.close();
|
||||
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to release file lock
|
||||
if (fileLock != null) {
|
||||
fileLock.release();
|
||||
}
|
||||
|
||||
// try to close lock file RAF object
|
||||
if (lockRAF != null) {
|
||||
lockRAF.close();
|
||||
}
|
||||
|
||||
// try to delete lock file
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param message message received by first instance from subsequent instances
|
||||
*/
|
||||
protected abstract void receiveMessage(String message);
|
||||
|
||||
/**
|
||||
* Method used in subsequent instances to send message to first instance.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @return message sent from subsequent instances
|
||||
*/
|
||||
protected abstract String sendMessage();
|
||||
|
||||
/**
|
||||
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
||||
*
|
||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||
*/
|
||||
protected void handleException(Exception exception) {
|
||||
public final String applicationId;
|
||||
private final boolean autoExit;
|
||||
|
||||
private Selector selector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
|
||||
public Instance(final String applicationId) {
|
||||
this(applicationId, true);
|
||||
}
|
||||
|
||||
public Instance(final String applicationId, final boolean autoExit) {
|
||||
this.applicationId = applicationId;
|
||||
this.autoExit = autoExit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain lock. If not possible, send data to first instance.
|
||||
*
|
||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||
*/
|
||||
public void acquireLock(boolean findExisting) throws InstanceException {
|
||||
Path lockFile = getLockFile(findExisting);
|
||||
|
||||
if(!Files.exists(lockFile)) {
|
||||
startServer(lockFile);
|
||||
} else {
|
||||
doClient(lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void startServer(Path lockFile) throws InstanceException {
|
||||
try {
|
||||
selector = Selector.open();
|
||||
UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(lockFile);
|
||||
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
|
||||
serverChannel.bind(socketAddress);
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
lockFile.toFile().deleteOnExit();
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException("Could not open UNIX socket lock file for instance at " + lockFile.toAbsolutePath(), e);
|
||||
}
|
||||
|
||||
Thread thread = new Thread(() -> {
|
||||
while(true) {
|
||||
try {
|
||||
selector.select();
|
||||
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> iter = selectedKeys.iterator();
|
||||
while(iter.hasNext()) {
|
||||
SelectionKey key = iter.next();
|
||||
if(key.isAcceptable()) {
|
||||
SocketChannel client = serverChannel.accept();
|
||||
client.configureBlocking(false);
|
||||
client.register(selector, SelectionKey.OP_READ);
|
||||
}
|
||||
if(key.isReadable()) {
|
||||
try(SocketChannel clientChannel = (SocketChannel)key.channel()) {
|
||||
String message = readMessage(clientChannel);
|
||||
clientChannel.write(ByteBuffer.wrap(applicationId.getBytes(StandardCharsets.UTF_8)));
|
||||
receiveMessage(message);
|
||||
}
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
} catch(SocketException e) {
|
||||
if(serverChannel.isOpen()) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread.setDaemon(true);
|
||||
thread.setName("SparrowInstanceListener");
|
||||
thread.start();
|
||||
|
||||
createSymlink(lockFile);
|
||||
}
|
||||
|
||||
private void doClient(Path lockFile) throws InstanceException {
|
||||
try(SocketChannel client = SocketChannel.open(UnixDomainSocketAddress.of(lockFile))) {
|
||||
String message = sendMessage();
|
||||
client.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
|
||||
client.shutdownOutput();
|
||||
|
||||
String response = readMessage(client);
|
||||
if(response.equals(applicationId) && autoExit) {
|
||||
beforeExit();
|
||||
System.exit(0);
|
||||
}
|
||||
} catch(ConnectException e) {
|
||||
try {
|
||||
Files.deleteIfExists(lockFile);
|
||||
startServer(lockFile);
|
||||
} catch(Exception ex) {
|
||||
throw new InstanceException("Could not delete lock file from previous instance", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException("Could not open client connection to existing instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String readMessage(SocketChannel clientChannel) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
StringBuilder messageBuilder = new StringBuilder();
|
||||
while(clientChannel.read(buffer) != -1) {
|
||||
buffer.flip();
|
||||
messageBuilder.append(new String(buffer.array(), 0, buffer.limit()));
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
return messageBuilder.toString();
|
||||
}
|
||||
|
||||
private Path getLockFile(boolean findExisting) {
|
||||
if(findExisting) {
|
||||
Path pointer = getUserLockFilePointer();
|
||||
try {
|
||||
if(pointer != null && Files.exists(pointer)) {
|
||||
if(Files.isSymbolicLink(pointer)) {
|
||||
return Files.readSymbolicLink(pointer);
|
||||
} else {
|
||||
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
|
||||
if(Files.exists(lockFile)) {
|
||||
return lockFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
|
||||
}
|
||||
|
||||
private void createSymlink(Path lockFile) {
|
||||
Path pointer = getUserLockFilePointer();
|
||||
try {
|
||||
if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createSymbolicLink(pointer, lockFile);
|
||||
pointer.toFile().deleteOnExit();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.debug("Could not create symlink " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath() + ", writing as normal file", e);
|
||||
|
||||
try {
|
||||
Files.writeString(pointer, lockFile.toAbsolutePath().toString(), StandardCharsets.UTF_8);
|
||||
pointer.toFile().deleteOnExit();
|
||||
} catch(IOException ex) {
|
||||
log.warn("Could not create pointer " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath(), ex);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
private Path getUserLockFilePointer() {
|
||||
if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
File sparrowHome = Storage.getSparrowHome(true);
|
||||
if(!sparrowHome.exists()) {
|
||||
Storage.createOwnerOnlyDirectory(sparrowHome);
|
||||
}
|
||||
|
||||
return sparrowHome.toPath().resolve(applicationId + ".default");
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||
*/
|
||||
public void freeLock() throws InstanceException {
|
||||
try {
|
||||
if(serverChannel != null && serverChannel.isOpen()) {
|
||||
serverChannel.close();
|
||||
}
|
||||
if(getUserLockFilePointer() != null) {
|
||||
Files.deleteIfExists(getUserLockFilePointer());
|
||||
}
|
||||
Files.deleteIfExists(getLockFile(false));
|
||||
} catch(Exception e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param message message received by first instance from subsequent instances
|
||||
*/
|
||||
protected abstract void receiveMessage(String message);
|
||||
|
||||
/**
|
||||
* Method used in subsequent instances to send message to first instance.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @return message sent from subsequent instances
|
||||
*/
|
||||
protected abstract String sendMessage();
|
||||
|
||||
/**
|
||||
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
||||
*
|
||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||
*/
|
||||
protected void handleException(Exception exception) {
|
||||
log.error("Error listening for instances", exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before exiting from subsequent instances.<br><br>
|
||||
*
|
||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||
* This method is not invoked if auto exit is turned off.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @since 1.2
|
||||
*/
|
||||
protected void beforeExit() {}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before exiting from subsequent instances.<br><br>
|
||||
*
|
||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||
* This method is not invoked if auto exit is turned off.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*/
|
||||
protected void beforeExit() {}
|
||||
}
|
||||
|
||||
@@ -7,88 +7,14 @@ import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* The <code>InstanceList</code> class is a logical entry point to the library which extends the functionality of the <code>Instance</code> class.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // unique application ID
|
||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
||||
*
|
||||
* // create Instance instance
|
||||
* Instance unique = new InstanceList(APP_ID) {
|
||||
* @Override
|
||||
* protected List<String> sendMessageList() {
|
||||
* List<String> messageList = new ArrayList<String>();
|
||||
*
|
||||
* messageList.add("Message 1");
|
||||
* messageList.add("Message 2");
|
||||
* messageList.add("Message 3");
|
||||
* messageList.add("Message 4");
|
||||
*
|
||||
* return messageList;
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void receiveMessageList(List<String> messageList) {
|
||||
* for (String message : messageList) {
|
||||
* System.out.println(message);
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.3
|
||||
*
|
||||
*/
|
||||
public abstract class InstanceList extends Instance {
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
*/
|
||||
public InstanceList(String APP_ID) {
|
||||
super(APP_ID);
|
||||
public InstanceList(String applicationId) {
|
||||
super(applicationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
||||
*/
|
||||
public InstanceList(String APP_ID, boolean AUTO_EXIT) {
|
||||
super(APP_ID, AUTO_EXIT);
|
||||
public InstanceList(String applicationId, boolean autoExit) {
|
||||
super(applicationId, autoExit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,16 +27,15 @@ public abstract class InstanceList extends Instance {
|
||||
*/
|
||||
@Override
|
||||
protected final void receiveMessage(String message) {
|
||||
if (message == null) {
|
||||
if(message == null) {
|
||||
receiveMessageList(null);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// parse the JSON array string into an array of string arguments
|
||||
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
||||
|
||||
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
||||
|
||||
for (int i = 0; i < jsonArgs.size(); i++) {
|
||||
for(int i = 0; i < jsonArgs.size(); i++) {
|
||||
JsonElement element = jsonArgs.get(i);
|
||||
stringArgs.add(element.getAsString());
|
||||
}
|
||||
@@ -137,10 +62,11 @@ public abstract class InstanceList extends Instance {
|
||||
JsonArray jsonArgs = new JsonArray();
|
||||
|
||||
List<String> stringArgs = sendMessageList();
|
||||
if(stringArgs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stringArgs == null) return null;
|
||||
|
||||
for (String arg : stringArgs) {
|
||||
for(String arg : stringArgs) {
|
||||
jsonArgs.add(arg);
|
||||
}
|
||||
|
||||
@@ -168,5 +94,4 @@ public abstract class InstanceList extends Instance {
|
||||
* @return list of messages sent from subsequent instances
|
||||
*/
|
||||
protected abstract List<String> sendMessageList();
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Unchained or Caravan Multisig";
|
||||
return "Unchained Caravan Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,11 +56,11 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
Keystore keystore = new Keystore(extKey.name.length() > Keystore.MAX_LABEL_LENGTH ? extKey.name.substring(0, Keystore.MAX_LABEL_LENGTH) : extKey.name);
|
||||
|
||||
if("Unknown".equals(extKey.bip32Path)) {
|
||||
extKey.bip32Path = "m/45'/0/0/0";
|
||||
extKey.bip32Path = "m/0/0/0/0";
|
||||
}
|
||||
|
||||
try {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path, true));
|
||||
} catch(NumberFormatException e) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, scriptType.getDefaultDerivationPath()));
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
CaravanFile cf = new CaravanFile();
|
||||
cf.name = wallet.getFullName();
|
||||
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
|
||||
cf.network = Network.get().getName();
|
||||
cf.network = Network.getCanonical().getName();
|
||||
cf.client = new Client();
|
||||
|
||||
Quorum quorum = new Quorum();
|
||||
|
||||
@@ -48,7 +48,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(coboKeystore.ExtPubKey);
|
||||
|
||||
@@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.Utils;
|
||||
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.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -36,6 +33,31 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(baos);
|
||||
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
|
||||
Keystore keystore;
|
||||
try {
|
||||
keystore = getKeystoreMultisig(scriptType, firstClone, password);
|
||||
} catch(Exception e) {
|
||||
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
|
||||
}
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Error importing keystore for " + scriptType, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
|
||||
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
|
||||
}
|
||||
|
||||
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
@@ -49,16 +71,16 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
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.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
} else {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
@@ -86,7 +108,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > " + account + " feature on your Coldcard.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Settings > Multisig Wallets > Export XPUB > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,7 +157,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(value));
|
||||
wallet.makeLabelsUnique(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
@@ -149,8 +171,10 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
wallet.setDefaultPolicy(policy);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
if(!wallet.isValid()) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
@@ -161,7 +185,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard.";
|
||||
return "Import file or QR created by using Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export on your Coldcard.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +238,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from File feature.";
|
||||
return "Export file or QR that can be read by your Coldcard using Settings > Multisig Wallets > Import from File or QR.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > " + account + " feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Advanced > MicroSD > Export Wallet > Generic JSON > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,13 +72,13 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
|
||||
|
||||
if(ck.name != null) {
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("p2sh-p2wpkh", "p2sh_p2wpkh").toUpperCase(Locale.ROOT));
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("-", "_").toUpperCase(Locale.ROOT));
|
||||
if(ckScriptType.equals(scriptType)) {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
|
||||
|
||||
return keystore;
|
||||
|
||||
@@ -77,7 +77,6 @@ public class Config {
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean sameAppMixing;
|
||||
private boolean mempoolFullRbf;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
@@ -662,15 +661,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isSameAppMixing() {
|
||||
return sameAppMixing;
|
||||
}
|
||||
|
||||
public void setSameAppMixing(boolean sameAppMixing) {
|
||||
this.sameAppMixing = sameAppMixing;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isMempoolFullRbf() {
|
||||
return mempoolFullRbf;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
keystore.setWalletModel(WalletModel.ELECTRUM);
|
||||
}
|
||||
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath, true));
|
||||
keystore.setExtendedPublicKey(xPub);
|
||||
keystore.setLabel(ek.label != null ? ek.label : "Electrum");
|
||||
if(keystore.getLabel().length() > Keystore.MAX_LABEL_LENGTH) {
|
||||
@@ -328,8 +328,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
ek.derivation = keystore.getKeyDerivation().getDerivationPath();
|
||||
ek.root_fingerprint = keystore.getKeyDerivation().getMasterFingerprint();
|
||||
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
if(keystore.getWalletModel().hasUsb()) {
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
} else {
|
||||
ek.type = "bip32";
|
||||
}
|
||||
ew.use_encryption = false;
|
||||
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||
ek.type = "bip32";
|
||||
|
||||
@@ -36,7 +36,7 @@ public class GordianSeedTool implements KeystoreFileImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.getCanonical().getCapitalizedName() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,7 +38,7 @@ public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String HWI_HOME_DIR = "hwi";
|
||||
private static final String HWI_VERSION_PREFIX = "hwi-";
|
||||
private static final String HWI_VERSION = "2.4.0";
|
||||
private static final String HWI_VERSION = "3.1.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
@@ -601,7 +601,7 @@ public class Hwi {
|
||||
private void addChainType(List<String> elements, boolean commandPresent) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.get()));
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.getCanonical()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreMnemonicShareImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -45,4 +46,25 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file or QR created by using Settings > Bitcoin > Multisig > [Wallet Detail] > Export via QR/microSD on your Passport.";
|
||||
}
|
||||
|
||||
@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", "Passport"));
|
||||
keystore.setWalletModel(WalletModel.PASSPORT);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
@@ -39,7 +42,7 @@ public class PdfUtils {
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur) {
|
||||
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur, BBQR bbqr) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PDF");
|
||||
@@ -56,9 +59,22 @@ public class PdfUtils {
|
||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||
document.add(title);
|
||||
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
String fragment = urEncoder.nextPart();
|
||||
if(urEncoder.isSinglePart()) {
|
||||
String fragment = null;
|
||||
if(bbqr != null) {
|
||||
BBQREncoder bbqrEncoder = new BBQREncoder(bbqr.type(), BBQREncoding.ZLIB, bbqr.data(), 2000, 0);
|
||||
if(bbqrEncoder.isSinglePart()) {
|
||||
fragment = bbqrEncoder.nextPart();
|
||||
}
|
||||
}
|
||||
|
||||
if(fragment == null) {
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
if(urEncoder.isSinglePart()) {
|
||||
fragment = urEncoder.nextPart();
|
||||
}
|
||||
}
|
||||
|
||||
if(fragment != null) {
|
||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||
document.add(image);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.SamouraiUtil;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class Samourai implements KeystoreFileImport {
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the wallet backup file samourai.txt exported from the Samourai app. Note that see the full balance, several script types may need to imported in separate wallets.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
|
||||
Gson gson = new Gson();
|
||||
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
|
||||
}.getType();
|
||||
Map<String, JsonElement> map = gson.fromJson(input, stringStringMap);
|
||||
|
||||
String payload = input;
|
||||
if(map.containsKey("payload")) {
|
||||
payload = map.get("payload").getAsString();
|
||||
}
|
||||
|
||||
int version = 1;
|
||||
if(map.containsKey("version")) {
|
||||
version = map.get("version").getAsInt();
|
||||
}
|
||||
|
||||
String decrypted;
|
||||
if(version == 1) {
|
||||
decrypted = SamouraiUtil.decrypt(payload, password, SamouraiUtil.DefaultPBKDF2Iterations);
|
||||
} else if(version == 2) {
|
||||
decrypted = SamouraiUtil.decryptSHA256(payload, password);
|
||||
} else {
|
||||
throw new ImportException("Unsupported backup version: " + version);
|
||||
}
|
||||
|
||||
SamouraiBackup backup = gson.fromJson(decrypted, SamouraiBackup.class);
|
||||
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes(backup.wallet.seed), password, 0);
|
||||
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
|
||||
keystore.setLabel(getWalletModel().toDisplayString());
|
||||
return keystore;
|
||||
} catch(JsonParseException e) {
|
||||
throw new ImportException("Failed to decrypt the wallet backup file, check if the password is correct.");
|
||||
} catch(ImportException e) {
|
||||
throw e;
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error importing backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Samourai Backup";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SAMOURAI;
|
||||
}
|
||||
|
||||
private static class SamouraiBackup {
|
||||
public SamouraiWallet wallet;
|
||||
}
|
||||
|
||||
private static class SamouraiWallet {
|
||||
public boolean testnet;
|
||||
public String seed;
|
||||
public String fingerprint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.RecoveryState;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class Slip39 implements KeystoreMnemonicShareImport {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Mnemonic Shares (SLIP39)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import your 20 or 33 word mnemonic shares and optional passphrase.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
|
||||
try {
|
||||
RecoveryState recoveryState = new RecoveryState();
|
||||
for(List<String> mnemonicWords : mnemonicShares) {
|
||||
Share share = Share.fromMnemonic(String.join(" ", mnemonicWords));
|
||||
recoveryState.addShare(share);
|
||||
}
|
||||
|
||||
if(recoveryState.isComplete()) {
|
||||
byte[] secret = recoveryState.recover(passphrase.getBytes(StandardCharsets.UTF_8));
|
||||
DeterministicSeed seed = new DeterministicSeed(secret, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.SLIP39);
|
||||
return Keystore.fromSeed(seed, derivation);
|
||||
} else {
|
||||
throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus());
|
||||
}
|
||||
} catch(MnemonicException e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Slip39ProgressException extends ImportException {
|
||||
private final String title;
|
||||
|
||||
public Slip39ProgressException(String title, String message) {
|
||||
super(message);
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
WalletModel walletModel = device.getWalletModel();
|
||||
if(walletModel != null) {
|
||||
keystore.setWalletModel(walletModel);
|
||||
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.TREZOR_SAFE_3 || walletModel == WalletModel.KEEPKEY ||
|
||||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X || walletModel == WalletModel.LEDGER_NANO_S_PLUS ||
|
||||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
|
||||
if(walletModel.hasUsb()) {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
} else if(walletModel == WalletModel.BITCOIN_CORE) {
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
|
||||
@@ -8,8 +8,6 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -21,6 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.cert.Certificate;
|
||||
@@ -174,15 +173,6 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
@@ -227,8 +217,8 @@ public class Storage {
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.getCanonical()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.getCanonical() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,10 +525,23 @@ public class Storage {
|
||||
return certsDir;
|
||||
}
|
||||
|
||||
static File getSparrowDir() {
|
||||
public static File getSparrowDir() {
|
||||
File sparrowDir;
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
||||
Network network = Network.get();
|
||||
if(network != Network.MAINNET) {
|
||||
sparrowDir = new File(getSparrowHome(), network.getHome());
|
||||
if(!network.getName().equals(network.getHome()) && !sparrowDir.exists()) {
|
||||
File networkNameDir = new File(getSparrowHome(), network.getName());
|
||||
if(networkNameDir.exists() && networkNameDir.isDirectory() && !Files.isSymbolicLink(networkNameDir.toPath())) {
|
||||
try {
|
||||
if(networkNameDir.renameTo(sparrowDir) && !isWindows()) {
|
||||
Files.createSymbolicLink(networkNameDir.toPath(), Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error creating symlink from " + networkNameDir.getAbsolutePath() + " to " + sparrowDir.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sparrowDir = getSparrowHome();
|
||||
}
|
||||
@@ -547,15 +550,36 @@ public class Storage {
|
||||
createOwnerOnlyDirectory(sparrowDir);
|
||||
}
|
||||
|
||||
if(!network.getName().equals(network.getHome()) && !isWindows()) {
|
||||
try {
|
||||
Path networkNamePath = getSparrowHome().toPath().resolve(network.getName());
|
||||
if(Files.isSymbolicLink(networkNamePath)) {
|
||||
Path symlinkTarget = getSparrowHome().toPath().resolve(Files.readSymbolicLink(networkNamePath));
|
||||
if(!Files.isSameFile(sparrowDir.toPath(), symlinkTarget)) {
|
||||
Files.delete(networkNamePath);
|
||||
Files.createSymbolicLink(networkNamePath, Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} else if(!Files.exists(networkNamePath)) {
|
||||
Files.createSymbolicLink(networkNamePath, Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error updating symlink from " + network.getName() + " to " + sparrowDir.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return sparrowDir;
|
||||
}
|
||||
|
||||
public static File getSparrowHome() {
|
||||
if(System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) {
|
||||
return getSparrowHome(false);
|
||||
}
|
||||
|
||||
public static File getSparrowHome(boolean useDefault) {
|
||||
if(!useDefault && System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) {
|
||||
return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY));
|
||||
}
|
||||
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
return new File(getHomeDir(), WINDOWS_SPARROW_DIR);
|
||||
}
|
||||
|
||||
@@ -563,7 +587,7 @@ public class Storage {
|
||||
}
|
||||
|
||||
static File getHomeDir() {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
return new File(System.getenv("APPDATA"));
|
||||
}
|
||||
|
||||
@@ -572,7 +596,7 @@ public class Storage {
|
||||
|
||||
public static boolean createOwnerOnlyDirectory(File directory) {
|
||||
try {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
Files.createDirectories(directory.toPath());
|
||||
return true;
|
||||
}
|
||||
@@ -590,7 +614,7 @@ public class Storage {
|
||||
|
||||
public static boolean createOwnerOnlyFile(File file) {
|
||||
try {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
Files.createFile(file.toPath());
|
||||
return true;
|
||||
}
|
||||
@@ -621,6 +645,10 @@ public class Storage {
|
||||
return ownerOnly;
|
||||
}
|
||||
|
||||
private static boolean isWindows() {
|
||||
return Platform.getCurrent() == Platform.WINDOWS;
|
||||
}
|
||||
|
||||
public static class LoadWalletService extends Service<WalletAndKey> {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.csvreader.CsvReader;
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -122,7 +124,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Imports a file containing labels in the BIP329 standard format to the currently selected wallet.";
|
||||
return "Imports a file containing labels in the BIP329 standard (or Electrum history CSV) format to the currently selected wallet.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,7 +143,17 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
try {
|
||||
label = gson.fromJson(line, Label.class);
|
||||
} catch(Exception e) {
|
||||
continue;
|
||||
//Try parse Electrum history CSV, or any CSV with txid,label entries
|
||||
try {
|
||||
CsvReader csvReader = new CsvReader(new StringReader(line));
|
||||
if(csvReader.readRecord() && csvReader.getColumnCount() > 1 && csvReader.get(0).length() == 64 && Utils.isHex(csvReader.get(0))) {
|
||||
label = new Label(Type.tx, csvReader.get(0), csvReader.get(1), null, null);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(label == null || label.type == null || label.ref == null) {
|
||||
|
||||
@@ -31,6 +31,10 @@ public class WalletTransactions implements WalletExport {
|
||||
|
||||
private final WalletForm walletForm;
|
||||
|
||||
static {
|
||||
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public WalletTransactions(WalletForm walletForm) {
|
||||
this.walletForm = walletForm;
|
||||
}
|
||||
@@ -72,7 +76,7 @@ public class WalletTransactions implements WalletExport {
|
||||
try {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
|
||||
writer.write("Date");
|
||||
writer.write("Date (UTC)");
|
||||
writer.write("Label");
|
||||
writer.write("Value");
|
||||
writer.write("Balance");
|
||||
|
||||
@@ -37,6 +37,7 @@ public class BBQRDecoder {
|
||||
|
||||
if(receivedParts.size() == totalParts) {
|
||||
byte[] data = concatParts();
|
||||
data = header.inflate(data);
|
||||
|
||||
if(type == BBQRType.PSBT) {
|
||||
result = new Result(new PSBT(data));
|
||||
|
||||
@@ -35,7 +35,7 @@ public class BBQREncoder {
|
||||
BBQREncoding encoding = desiredEncoding;
|
||||
|
||||
try {
|
||||
encoded = encoding.encode(data);
|
||||
encoded = encoding.encode(encoding.deflate(data));
|
||||
if(encoding == BBQREncoding.ZLIB) {
|
||||
String uncompressed = BBQREncoding.BASE32.encode(data);
|
||||
if(encoded.length() > uncompressed.length()) {
|
||||
|
||||
@@ -42,6 +42,11 @@ public enum BBQREncoding {
|
||||
}, ZLIB("Z") {
|
||||
@Override
|
||||
public String encode(byte[] data) throws BBQREncodingException {
|
||||
return BASE32.encode(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] deflate(byte[] data) throws BBQREncodingException {
|
||||
try {
|
||||
Deflater deflater = new Deflater(JZlib.Z_BEST_COMPRESSION, 10, true);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -49,7 +54,7 @@ public enum BBQREncoding {
|
||||
zOut.write(data);
|
||||
zOut.close();
|
||||
|
||||
return BASE32.encode(out.toByteArray());
|
||||
return out.toByteArray();
|
||||
} catch(Exception e) {
|
||||
throw new BBQREncodingException("Error deflating with zlib", e);
|
||||
}
|
||||
@@ -57,9 +62,14 @@ public enum BBQREncoding {
|
||||
|
||||
@Override
|
||||
public byte[] decode(String part) throws BBQREncodingException {
|
||||
return BASE32.decode(part);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] inflate(byte[] data) throws BBQREncodingException {
|
||||
try {
|
||||
Inflater inflater = new Inflater(10, true);
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(BASE32.decode(part));
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(data);
|
||||
InflaterInputStream zIn = new InflaterInputStream(in, inflater);
|
||||
byte[] decoded = zIn.readAllBytes();
|
||||
zIn.close();
|
||||
@@ -96,6 +106,14 @@ public enum BBQREncoding {
|
||||
return code;
|
||||
}
|
||||
|
||||
public byte[] deflate(byte[] data) throws BBQREncodingException {
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] inflate(byte[] data) throws BBQREncodingException {
|
||||
return data;
|
||||
}
|
||||
|
||||
public abstract String encode(byte[] data) throws BBQREncodingException;
|
||||
|
||||
public abstract byte[] decode(String part) throws BBQREncodingException;
|
||||
|
||||
@@ -13,6 +13,10 @@ public record BBQRHeader(BBQREncoding encoding, BBQRType type, int seqTotal, int
|
||||
return encoding.decode(part.substring(8));
|
||||
}
|
||||
|
||||
public byte[] inflate(byte[] data) {
|
||||
return encoding.inflate(data);
|
||||
}
|
||||
|
||||
public static BBQRHeader fromString(String part) {
|
||||
if(part.length() < 8) {
|
||||
throw new IllegalArgumentException("Part too short");
|
||||
|
||||
@@ -256,7 +256,10 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
for(Sha256Hash txid : referencedTxIds) {
|
||||
BlockTransaction blkTx = wallet.getTransactions().get(txid);
|
||||
blockTransactionDao.addOrUpdate(wallet, txid, blkTx);
|
||||
//May be null for a nested wallet if still updating
|
||||
if(blkTx != null) {
|
||||
blockTransactionDao.addOrUpdate(wallet, txid, blkTx);
|
||||
}
|
||||
}
|
||||
if(!dirtyPersistables.clearHistory) {
|
||||
DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class);
|
||||
|
||||
@@ -63,7 +63,7 @@ public interface KeystoreDao {
|
||||
long id = insertSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
} else {
|
||||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ public interface KeystoreDao {
|
||||
EncryptedData data = seed.getEncryptedData();
|
||||
updateSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
} else {
|
||||
updateSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
updateSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class SatoCardApi extends CardApi {
|
||||
String masterXpub = this.cardProtocol.cardBip32GetXpub("m", xtype);
|
||||
ExtendedKey masterExtendedKey = ExtendedKey.fromDescriptor(masterXpub);
|
||||
String masterFingerprint = Utils.bytesToHex(masterExtendedKey.getKey().getFingerprint());
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString);
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString, true);
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.SATOCHIP.toDisplayString());
|
||||
|
||||
@@ -31,6 +31,7 @@ public class KeystoreImportController implements Initializable {
|
||||
@FXML
|
||||
private StackPane importPane;
|
||||
|
||||
private KeyDerivation defaultDerivation;
|
||||
private KeyDerivation requiredDerivation;
|
||||
private WalletModel requiredModel;
|
||||
|
||||
@@ -110,6 +111,14 @@ public class KeystoreImportController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public KeyDerivation getDefaultDerivation() {
|
||||
return defaultDerivation;
|
||||
}
|
||||
|
||||
public void setDefaultDerivation(KeyDerivation defaultDerivation) {
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
}
|
||||
|
||||
public KeyDerivation getRequiredDerivation() {
|
||||
return requiredDerivation;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
this(wallet, initialSource, null, null, false);
|
||||
}
|
||||
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation requiredDerivation, WalletModel requiredModel, boolean restrictSource) {
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation currentDerivation, WalletModel currentModel, boolean restrictImport) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -43,12 +43,15 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
|
||||
keystoreImportController = ksiLoader.getController();
|
||||
keystoreImportController.initializeView(wallet);
|
||||
keystoreImportController.selectSource(initialSource, restrictSource);
|
||||
keystoreImportController.setRequiredDerivation(requiredDerivation);
|
||||
keystoreImportController.setRequiredModel(requiredModel);
|
||||
keystoreImportController.selectSource(initialSource, restrictImport);
|
||||
keystoreImportController.setDefaultDerivation(currentDerivation);
|
||||
if(restrictImport) {
|
||||
keystoreImportController.setRequiredDerivation(currentDerivation);
|
||||
keystoreImportController.setRequiredModel(currentModel);
|
||||
}
|
||||
|
||||
final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType(Network.get().getXpubHeader().getDisplayName() + " / Watch Only Wallet", ButtonBar.ButtonData.LEFT);
|
||||
if(!restrictSource) {
|
||||
if(!restrictImport) {
|
||||
dialogPane.getButtonTypes().add(watchOnlyButtonType);
|
||||
}
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.MnemonicKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.control.XprvKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Accordion;
|
||||
@@ -15,7 +12,7 @@ public class SwController extends KeystoreImportDetailController {
|
||||
private Accordion importAccordion;
|
||||
|
||||
public void initializeView() {
|
||||
List<KeystoreImport> importers = List.of(new Bip39(), new Electrum(), new Bip32());
|
||||
List<KeystoreImport> importers = List.of(new Bip39(), new Bip32(), new Slip39());
|
||||
|
||||
for(KeystoreImport importer : importers) {
|
||||
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
|
||||
@@ -27,9 +24,11 @@ public class SwController extends KeystoreImportDetailController {
|
||||
if(importer instanceof KeystoreFileImport) {
|
||||
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer, getMasterController().getRequiredDerivation());
|
||||
} else if(importer instanceof KeystoreMnemonicImport) {
|
||||
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer);
|
||||
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else if(importer instanceof KeystoreXprvImport) {
|
||||
importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer);
|
||||
importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else if(importer instanceof KeystoreMnemonicShareImport) {
|
||||
importPane = new MnemonicShareKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicShareImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass());
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class Auth47 {
|
||||
private String srbnName;
|
||||
private String resource;
|
||||
|
||||
public Auth47(URI uri) throws MalformedURLException {
|
||||
public Auth47(URI uri) throws MalformedURLException, URISyntaxException {
|
||||
this.nonce = uri.getHost();
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
@@ -64,12 +64,12 @@ public class Auth47 {
|
||||
}
|
||||
if(strCallback.startsWith(SRBN_PROTOCOL)) {
|
||||
String srbnCallback = HTTPS_PROTOCOL + strCallback.substring(SRBN_PROTOCOL.length());
|
||||
URL srbnUrl = new URL(srbnCallback);
|
||||
URL srbnUrl = new URI(srbnCallback).toURL();
|
||||
this.srbn = true;
|
||||
this.srbnName = srbnUrl.getUserInfo();
|
||||
this.callback = new URL(HTTPS_PROTOCOL + srbnUrl.getHost());
|
||||
this.callback = new URI(HTTPS_PROTOCOL + srbnUrl.getHost()).toURL();
|
||||
} else {
|
||||
this.callback = new URL(strCallback);
|
||||
this.callback = new URI(strCallback).toURL();
|
||||
}
|
||||
|
||||
this.expiry = parameterMap.get("e");
|
||||
|
||||
@@ -5,14 +5,15 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
|
||||
public enum BroadcastSource {
|
||||
@@ -28,11 +29,11 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET, Network.TESTNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -46,16 +47,18 @@ public enum BroadcastSource {
|
||||
|
||||
@Override
|
||||
public List<Network> getSupportedNetworks() {
|
||||
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
|
||||
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET, Network.TESTNET4);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.SIGNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/signet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/signet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET4) {
|
||||
return new URI(getBaseUrl(proxy) + "/testnet4/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -72,11 +75,13 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.SIGNET) {
|
||||
return new URI(getBaseUrl(proxy) + "/signet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -93,11 +98,11 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -109,7 +114,6 @@ public enum BroadcastSource {
|
||||
private final String onionUrl;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BroadcastSource.class);
|
||||
private static final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
BroadcastSource(String name, String tlsUrl, String onionUrl) {
|
||||
this.name = name;
|
||||
@@ -137,7 +141,7 @@ public enum BroadcastSource {
|
||||
|
||||
public abstract List<Network> getSupportedNetworks();
|
||||
|
||||
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException;
|
||||
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException;
|
||||
|
||||
public Sha256Hash postTransactionData(String data) throws BroadcastException {
|
||||
//If a Tor proxy is configured, ensure we use a new circuit by configuring a random proxy password
|
||||
@@ -158,7 +162,7 @@ public enum BroadcastSource {
|
||||
} catch(Exception e) {
|
||||
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
|
||||
}
|
||||
} catch(JavaHttpException e) {
|
||||
} catch(HttpResponseException e) {
|
||||
throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody());
|
||||
} catch(Exception e) {
|
||||
log.error("Could not post transaction via " + getName(), e);
|
||||
|
||||
@@ -119,7 +119,7 @@ public class Bwt {
|
||||
*/
|
||||
private void start(Collection<String> outputDescriptors, Collection<String> addresses, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
|
||||
BwtConfig bwtConfig = new BwtConfig();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.getCanonical().getName();
|
||||
|
||||
if(!outputDescriptors.isEmpty()) {
|
||||
bwtConfig.descriptors = outputDescriptors;
|
||||
|
||||
@@ -812,12 +812,18 @@ public class ElectrumServer {
|
||||
return transactionMap;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks, boolean useCached) throws ServerException {
|
||||
Map<Integer, Double> targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks);
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(Network.get().equals(Network.MAINNET)) {
|
||||
if(!feeRatesSource.isExternal()) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
} else if(useCached) {
|
||||
if(AppServices.getTargetBlockFeeRates() != null) {
|
||||
targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates());
|
||||
}
|
||||
} else if(Network.get().equals(Network.MAINNET)) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
}
|
||||
|
||||
@@ -1204,7 +1210,7 @@ public class ElectrumServer {
|
||||
|
||||
String banner = electrumServer.getServerBanner();
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, true);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
|
||||
@@ -1220,7 +1226,7 @@ public class ElectrumServer {
|
||||
|
||||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||
if(elapsed > FEE_RATES_PERIOD) {
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
@@ -1679,9 +1685,8 @@ public class ElectrumServer {
|
||||
return new Task<>() {
|
||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -107,7 +107,7 @@ public enum ExchangeSource {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving historical currency rates", e);
|
||||
} else {
|
||||
if(e instanceof JavaHttpException javaHttpException && javaHttpException.getStatusCode() == 404) {
|
||||
if(e instanceof HttpResponseException httpException && httpException.getStatusCode() == 404) {
|
||||
log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this);
|
||||
} else {
|
||||
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
|
||||
|
||||
@@ -9,27 +9,27 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum FeeRatesSource {
|
||||
ELECTRUM_SERVER("Server") {
|
||||
ELECTRUM_SERVER("Server", false) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
},
|
||||
MEMPOOL_SPACE("mempool.space") {
|
||||
MEMPOOL_SPACE("mempool.space", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
|
||||
BITCOINFEES_EARN_COM("bitcoinfees.earn.com", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
MINIMUM("Minimum (1 sat/vB)") {
|
||||
MINIMUM("Minimum (1 sat/vB)", false) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
@@ -40,7 +40,7 @@ public enum FeeRatesSource {
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
},
|
||||
OXT_ME("oxt.me") {
|
||||
OXT_ME("oxt.me", true) {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = AppServices.isUsingProxy() ? "http://oxtwshnfyktikbflierkwcxxksbonl6v73l5so5zky7ur72w52tktkid.onion/stats/global/mempool" : "https://api.oxt.me/stats/global/mempool";
|
||||
@@ -63,9 +63,11 @@ public enum FeeRatesSource {
|
||||
public static final int BLOCKS_IN_TWO_HOURS = 12;
|
||||
|
||||
private final String name;
|
||||
private final boolean external;
|
||||
|
||||
FeeRatesSource(String name) {
|
||||
FeeRatesSource(String name, boolean external) {
|
||||
this.name = name;
|
||||
this.external = external;
|
||||
}
|
||||
|
||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||
@@ -74,6 +76,10 @@ public enum FeeRatesSource {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting fee rates from " + url);
|
||||
|
||||
@@ -1,57 +1,50 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.HttpUsage;
|
||||
import com.samourai.http.client.IHttpClient;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.sparrowwallet.sparrow.net.http.client.AsyncUtil;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.IHttpClient;
|
||||
import com.sparrowwallet.sparrow.net.http.client.JettyHttpClientService;
|
||||
import io.reactivex.Observable;
|
||||
import java8.util.Optional;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpClientService {
|
||||
private final JavaHttpClientService httpClientService;
|
||||
public class HttpClientService extends JettyHttpClientService {
|
||||
private static final int REQUEST_TIMEOUT = 120000;
|
||||
|
||||
public HttpClientService(HostAndPort torProxy) {
|
||||
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
|
||||
super(REQUEST_TIMEOUT, new HttpProxySupplier(torProxy));
|
||||
}
|
||||
|
||||
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.getJson(url, responseType, headers);
|
||||
return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers);
|
||||
}
|
||||
|
||||
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(url, responseType, headers, body);
|
||||
return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable();
|
||||
}
|
||||
|
||||
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postString(url, headers, contentType, content);
|
||||
}
|
||||
|
||||
public void changeIdentity() {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(torProxy != null) {
|
||||
TorUtils.changeIdentity(torProxy);
|
||||
}
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.DEFAULT);
|
||||
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
return getHttpProxySupplier().getTorProxy();
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
stop();
|
||||
getHttpProxySupplier()._setTorProxy(torProxy);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
httpClientService.shutdown();
|
||||
@Override
|
||||
public HttpProxySupplier getHttpProxySupplier() {
|
||||
return (HttpProxySupplier)super.getHttpProxySupplier();
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
@@ -65,7 +58,7 @@ public class HttpClientService {
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
httpClientService.shutdown();
|
||||
httpClientService.stop();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user