mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4eed965c | ||
|
|
c034dbe89e | ||
|
|
4cb2e1ef9f | ||
|
|
7258b049c9 | ||
|
|
5475a81e3a | ||
|
|
f003b6d732 | ||
|
|
c34a423f95 | ||
|
|
55e7689f7c | ||
|
|
914afe9a8d | ||
|
|
81c7bc7ecb | ||
|
|
3d18477560 | ||
|
|
2c1f591c51 | ||
|
|
d109eaa654 | ||
|
|
803e43cb45 | ||
|
|
a45024ac70 | ||
|
|
0f05502af6 | ||
|
|
6a496894e1 | ||
|
|
d3b0eac51a | ||
|
|
2cc02e38e6 | ||
|
|
ae29108656 | ||
|
|
0ed8c6af7c | ||
|
|
6d7f02227a | ||
|
|
8f52039c7b | ||
|
|
f14e2fb020 | ||
|
|
cc9a557a2e | ||
|
|
e50cc7126d | ||
|
|
6cdbba4bb3 | ||
|
|
7f3885611a | ||
|
|
17ea75603f | ||
|
|
da1626070b | ||
|
|
6f4d37d3ff | ||
|
|
78b0c63f87 | ||
|
|
14d96bdb4e | ||
|
|
d73820464e | ||
|
|
ff4ff90bb2 | ||
|
|
d2940265fd | ||
|
|
3f72a84afe | ||
|
|
521bbdd70e | ||
|
|
15c3150a4f | ||
|
|
865c52e5d9 | ||
|
|
ee195f2677 | ||
|
|
1d50b4f296 | ||
|
|
22310cd8c9 | ||
|
|
78406fd024 | ||
|
|
74a551ed01 | ||
|
|
4de6bd5e83 | ||
|
|
8d6230e834 | ||
|
|
31042039d7 | ||
|
|
1ba501f5c8 | ||
|
|
55d5a97d99 | ||
|
|
37af663511 | ||
|
|
b757c76028 | ||
|
|
82e0b63ed0 | ||
|
|
7da3a55489 | ||
|
|
7ebb92d90a | ||
|
|
1d32b69345 | ||
|
|
fbc49fd6f5 | ||
|
|
30001051c7 | ||
|
|
1e74ae5f19 | ||
|
|
6fc52fdc0e | ||
|
|
1d2081d2a6 | ||
|
|
4d587cf776 | ||
|
|
14689dd256 | ||
|
|
c8a5c1fb86 | ||
|
|
734818cd9e | ||
|
|
6aa5473b24 | ||
|
|
1b460533f5 | ||
|
|
ad1ecfb887 | ||
|
|
6a5060c0c8 | ||
|
|
02a0a3277b | ||
|
|
57dba5d6ae | ||
|
|
540424a2e3 | ||
|
|
e1d9d54da3 | ||
|
|
2b8a513adf | ||
|
|
20df1dbd8b | ||
|
|
04c8017bb5 | ||
|
|
be0ac52a09 | ||
|
|
3162371838 | ||
|
|
b5196d1ac2 | ||
|
|
343cb2271c | ||
|
|
4feb4a3a79 | ||
|
|
20b4d5a1b5 | ||
|
|
06489d8339 | ||
|
|
e368782b4c | ||
|
|
6ee3755ce4 | ||
|
|
d425933189 | ||
|
|
675b7ed4f8 | ||
|
|
6072f6d31a | ||
|
|
e1fb674170 | ||
|
|
d8839763a4 | ||
|
|
64e98cdb35 | ||
|
|
bde8fef35e | ||
|
|
87e7a87e5e | ||
|
|
85eb4df7e9 | ||
|
|
bb32a1e7b1 | ||
|
|
f590794589 | ||
|
|
1e3ce7eb88 | ||
|
|
ef3e2ed695 | ||
|
|
74c3370277 | ||
|
|
170e7c0abf | ||
|
|
e0486ff4a9 | ||
|
|
1d17384152 | ||
|
|
aec26d512b | ||
|
|
9e5a6e83d1 | ||
|
|
eecb90e9b2 | ||
|
|
b3516063b2 | ||
|
|
1d560d6aa6 | ||
|
|
d128401a09 | ||
|
|
9391a397da | ||
|
|
a5312374a8 | ||
|
|
c81c42a87c | ||
|
|
d84ade5b7d | ||
|
|
323d29e34a |
@@ -12,11 +12,11 @@ jobs:
|
||||
matrix:
|
||||
os: [windows-2022, ubuntu-20.04, macos-12]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
path: |
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
path: |
|
||||
|
||||
+50
-60
@@ -2,12 +2,12 @@ import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.26.0'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.8.0'
|
||||
def sparrowVersion = '1.8.3'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -63,18 +63,19 @@ java {
|
||||
|
||||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo')) {
|
||||
exclude group: 'org.hamcrest'
|
||||
exclude group: 'junit'
|
||||
}
|
||||
implementation('com.google.guava:guava:31.1-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation(project(':drongo'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
@@ -89,7 +90,7 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.7')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.3')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
@@ -118,25 +119,26 @@ dependencies {
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.37')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.40')
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('org.apache.commons:commons-compress:1.25.0')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
testImplementation('junit:junit:4.13.1')
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -157,10 +159,14 @@ processResources {
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '--add-opens=java.base/java.io=com.google.gson'
|
||||
useJUnitPlatform()
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
}
|
||||
|
||||
run {
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
@@ -236,7 +242,8 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
|
||||
"--add-reads=com.sparrowwallet.merged.module=ch.qos.logback.classic",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.slf4j",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
@@ -260,7 +267,7 @@ jlink {
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
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']
|
||||
@@ -405,41 +412,16 @@ extraJavaModuleInfo {
|
||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
|
||||
exports('org.slf4j.bridge')
|
||||
requires('java.logging')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
||||
exports('org.zeromq')
|
||||
}
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
}
|
||||
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
|
||||
exports('ch.qos.logback.classic')
|
||||
requires('org.slf4j')
|
||||
requires('logback.core')
|
||||
requires('java.xml')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
|
||||
exports('com.google.common.util.concurrent.internal')
|
||||
exports('org.json.simple.parser')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('guava-31.1-jre.jar', 'com.google.common', '31.1-jre') {
|
||||
exports('com.google.common.eventbus')
|
||||
exports('com.google.common.net')
|
||||
exports('com.google.common.base')
|
||||
exports('com.google.common.collect')
|
||||
exports('com.google.common.io')
|
||||
exports('com.google.common.primitives')
|
||||
exports('com.google.common.math')
|
||||
requires('failureaccess')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
|
||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
@@ -450,6 +432,7 @@ extraJavaModuleInfo {
|
||||
requires('io.leangen.geantyref')
|
||||
requires('java.sql')
|
||||
requires('org.slf4j')
|
||||
requires('com.github.benmanes.caffeine')
|
||||
}
|
||||
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
||||
exports('io.leangen.geantyref')
|
||||
@@ -508,7 +491,7 @@ extraJavaModuleInfo {
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.37.jar', 'com.sparrowwallet.nightjar', '0.2.37') {
|
||||
module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -516,7 +499,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('logback.classic')
|
||||
requires('ch.qos.logback.classic')
|
||||
requires('org.json')
|
||||
requires('io.reactivex.rxjava2')
|
||||
exports('com.samourai.http.client')
|
||||
@@ -719,15 +702,22 @@ extraJavaModuleInfo {
|
||||
exports('io.matthewnelson.component.parcelize')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('junit-4.12.jar', 'junit', '4.12') {
|
||||
exports('org.junit')
|
||||
requires('org.hamcrest.core')
|
||||
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')
|
||||
}
|
||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
||||
}
|
||||
@@ -3,9 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.12'
|
||||
implementation 'org.ow2.asm:asm:9.6'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -22,9 +20,5 @@ 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")) {
|
||||
if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* 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 )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not procede.
|
||||
If key doesn't match, do not proceed.
|
||||
|
||||
Add Adoptium PGP key to a the keyring shared folder:
|
||||
```sh
|
||||
@@ -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.7.9"
|
||||
GIT_TAG="1.8.2"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: 28551247c6...6868b026fb
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -83,10 +83,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -133,10 +131,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/pgp-signature
|
||||
extension=asc
|
||||
description=ASCII Armored File
|
||||
@@ -6,5 +6,5 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.0</string>
|
||||
<string>1.8.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -94,6 +94,21 @@
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.sparrowwallet.asc</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>asc</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>ASCII Armored File</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
@@ -77,6 +79,7 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
||||
public class AppController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||
@@ -107,6 +110,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem exportWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem renameWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteWallet;
|
||||
|
||||
@@ -195,7 +201,7 @@ public class AppController implements Initializable {
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private MenuItem restart;
|
||||
private Menu restart;
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
@@ -215,6 +221,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private SendToManyDialog sendToManyDialog;
|
||||
|
||||
private DownloadVerifierDialog downloadVerifierDialog;
|
||||
|
||||
private Tab previouslySelectedTab;
|
||||
|
||||
private boolean subTabsVisible;
|
||||
@@ -274,7 +282,7 @@ public class AppController implements Initializable {
|
||||
|
||||
rootStack.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
event.acceptTransferModes(TransferMode.LINK);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
@@ -283,9 +291,7 @@ public class AppController implements Initializable {
|
||||
Dragboard db = event.getDragboard();
|
||||
boolean success = false;
|
||||
if(db.hasFiles()) {
|
||||
for(File file : db.getFiles()) {
|
||||
openFile(file);
|
||||
}
|
||||
openFiles(db.getFiles());
|
||||
success = true;
|
||||
}
|
||||
event.setDropCompleted(success);
|
||||
@@ -372,7 +378,14 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
|
||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||
restart.setText("Restart in " + (Network.get() == Network.MAINNET ? Network.TESTNET.toDisplayString() : Network.MAINNET.toDisplayString()));
|
||||
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||
networks.remove(Network.get());
|
||||
for(Network network : networks) {
|
||||
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
||||
networkItem.setOnAction(event -> restart(event, network));
|
||||
restart.getItems().add(networkItem);
|
||||
}
|
||||
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
@@ -382,6 +395,7 @@ public class AppController implements Initializable {
|
||||
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
|
||||
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
exportWallet.setDisable(true);
|
||||
renameWallet.disableProperty().bind(exportWallet.disableProperty());
|
||||
deleteWallet.disableProperty().bind(exportWallet.disableProperty());
|
||||
closeTab.setDisable(true);
|
||||
lockWallet.setDisable(true);
|
||||
@@ -622,7 +636,7 @@ public class AppController implements Initializable {
|
||||
} catch(TransactionParseException e) {
|
||||
showErrorDialog("Invalid transaction", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Invalid file", e.getMessage());
|
||||
showErrorDialog("Invalid file", "Cannot recognise the format of the " + file.getName() + " file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -738,8 +752,10 @@ public class AppController implements Initializable {
|
||||
Transaction transaction = transactionTabData.getTransaction();
|
||||
|
||||
try {
|
||||
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
|
||||
byte[] txBytes = transaction.bitcoinSerialize();
|
||||
UR ur = UR.fromBytes(txBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
} catch(Exception e) {
|
||||
@@ -837,8 +853,10 @@ public class AppController implements Initializable {
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.show();
|
||||
}
|
||||
@@ -953,7 +971,7 @@ public class AppController implements Initializable {
|
||||
AppServices.get().setPreventSleep(item.isSelected());
|
||||
}
|
||||
|
||||
public void restart(ActionEvent event) {
|
||||
public void restart(ActionEvent event, Network network) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||
}
|
||||
@@ -964,7 +982,7 @@ public class AppController implements Initializable {
|
||||
jCommander.parse(argv);
|
||||
});
|
||||
|
||||
args.network = (Network.get() == Network.MAINNET ? Network.TESTNET : Network.MAINNET);
|
||||
args.network = network;
|
||||
|
||||
try {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
@@ -978,11 +996,19 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void openFile(File file) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
public void openFiles(List<File> files) {
|
||||
boolean verifyOpened = false;
|
||||
for(File file : files) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
} else if(isVerifyDownloadFile(file)) {
|
||||
if(!verifyOpened) {
|
||||
verifyDownload(new ActionEvent(file, rootStack));
|
||||
verifyOpened = true;
|
||||
}
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1299,7 +1325,7 @@ public class AppController implements Initializable {
|
||||
public void exportWallet(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm.getWallet());
|
||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Wallet> wallet = dlg.showAndWait();
|
||||
if(wallet.isPresent()) {
|
||||
@@ -1464,6 +1490,23 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyDownload(ActionEvent event) {
|
||||
if(downloadVerifierDialog != null) {
|
||||
Stage stage = (Stage)downloadVerifierDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
if(event.getSource() instanceof File file) {
|
||||
downloadVerifierDialog.setSignatureFile(file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
downloadVerifierDialog = new DownloadVerifierDialog(event.getSource() instanceof File file ? file : null);
|
||||
downloadVerifierDialog.initOwner(rootStack.getScene().getWindow());
|
||||
downloadVerifierDialog.showAndWait();
|
||||
downloadVerifierDialog = null;
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
@@ -3025,6 +3068,13 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestVerifyDownloadOpen(RequestVerifyDownloadEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
verifyDownload(new ActionEvent(event.getFile(), rootStack));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void functionAction(FunctionActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
|
||||
@@ -24,7 +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.paynym.PayNymService;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
@@ -34,6 +33,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@@ -61,7 +61,6 @@ import java.awt.event.KeyEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -70,6 +69,8 @@ import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
||||
public class AppServices {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
|
||||
|
||||
@@ -80,13 +81,17 @@ public class AppServices {
|
||||
private static final int RATES_PERIOD_SECS = 5 * 60;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final int CONNECTION_DELAY_SECS = 2;
|
||||
private static final int RATES_DELAY_SECS_DEFAULT = 2;
|
||||
private static final int RATES_DELAY_SECS_WINDOWS = 5;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
public static final List<Long> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
||||
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
@@ -96,7 +101,7 @@ public class AppServices {
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
|
||||
private static PayNymService payNymService;
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
private final Application application;
|
||||
|
||||
@@ -247,8 +252,8 @@ public class AppServices {
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
|
||||
if(payNymService != null) {
|
||||
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
|
||||
if(httpClientService != null) {
|
||||
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
|
||||
shutdownService.start();
|
||||
}
|
||||
|
||||
@@ -355,6 +360,8 @@ public class AppServices {
|
||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
||||
//Delay startup on first run, Windows requires a longer delay
|
||||
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
|
||||
ratesService.setRestartOnFailure(true);
|
||||
|
||||
@@ -513,18 +520,18 @@ public class AppServices {
|
||||
return get().interactionServices;
|
||||
}
|
||||
|
||||
public static PayNymService getPayNymService() {
|
||||
if(payNymService == null) {
|
||||
public static HttpClientService getHttpClientService() {
|
||||
if(httpClientService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
payNymService = new PayNymService(torProxy);
|
||||
httpClientService = new HttpClientService(torProxy);
|
||||
} else {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
|
||||
payNymService.setTorProxy(getTorProxy());
|
||||
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
|
||||
httpClientService.setTorProxy(getTorProxy());
|
||||
}
|
||||
}
|
||||
|
||||
return payNymService;
|
||||
return httpClientService;
|
||||
}
|
||||
|
||||
public static HostAndPort getTorProxy() {
|
||||
@@ -662,15 +669,19 @@ public class AppServices {
|
||||
|
||||
public static Double getDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||
}
|
||||
|
||||
public static Double getMinimumFeeRate() {
|
||||
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
|
||||
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
|
||||
Double minRate = optMinFeeRate.orElse(getFallbackFeeRate());
|
||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static double getFallbackFeeRate() {
|
||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
@@ -776,7 +787,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Window getActiveWindow() {
|
||||
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : null);
|
||||
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : (Stage.getWindows().iterator().hasNext() ? Stage.getWindows().iterator().next() : null));
|
||||
}
|
||||
|
||||
public static void moveToActiveWindowScreen(Dialog<?> dialog) {
|
||||
@@ -849,6 +860,25 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFileUriArgumentsAfterWalletLoading(Window window) {
|
||||
if(!argFiles.isEmpty() || !argUris.isEmpty()) {
|
||||
Service<Void> service = new Service<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
Platform.runLater(() -> openFileUriArguments(window));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFileUriArguments(Window window) {
|
||||
openFiles(argFiles, window);
|
||||
argFiles.clear();
|
||||
@@ -875,6 +905,8 @@ public class AppServices {
|
||||
for(File file : openFiles) {
|
||||
if(isWalletFile(file)) {
|
||||
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
|
||||
} else if(isVerifyDownloadFile(file)) {
|
||||
EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file));
|
||||
} else {
|
||||
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import org.fxmisc.richtext.event.MouseOverTextEvent;
|
||||
import org.fxmisc.richtext.model.TwoDimensional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
|
||||
|
||||
public abstract class BaseController {
|
||||
@@ -24,14 +29,11 @@ public abstract class BaseController {
|
||||
|
||||
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
|
||||
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
|
||||
if(!hoverChunk.isOpCode()) {
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeScriptChunk(hoverChunk));
|
||||
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
ScriptChunk hoverChunk = getScriptChunk(scriptArea, e.getCharacterIndex());
|
||||
if(hoverChunk != null) {
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeScriptChunk(hoverChunk));
|
||||
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
});
|
||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
||||
@@ -80,4 +82,26 @@ public abstract class BaseController {
|
||||
|
||||
return "Invalid";
|
||||
}
|
||||
|
||||
public static ScriptChunk getScriptChunk(ScriptArea area, int characterIndex) {
|
||||
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(characterIndex, Backward);
|
||||
int ignoreCount = 0;
|
||||
for(int i = 0; i < position.getMajor() && i < area.getParagraph(0).getStyleSpans().getSpanCount(); i++) {
|
||||
Collection<String> styles = area.getParagraph(0).getStyleSpans().getStyleSpan(i).getStyle();
|
||||
if(i < position.getMajor() && (styles.contains("") || styles.contains("script-nest"))) {
|
||||
ignoreCount++;
|
||||
}
|
||||
}
|
||||
boolean hashScripts = List.of(P2PKH, P2SH, P2WPKH, P2WSH).stream().anyMatch(type -> type.isScriptType(area.getScript()));
|
||||
List<ScriptChunk> flatChunks = area.getScript().getChunks().stream().flatMap(chunk -> !hashScripts && chunk.isScript() ? chunk.getScript().getChunks().stream() : Stream.of(chunk)).collect(Collectors.toList());
|
||||
int chunkIndex = position.getMajor() - ignoreCount;
|
||||
if(chunkIndex < flatChunks.size()) {
|
||||
ScriptChunk chunk = flatChunks.get(chunkIndex);
|
||||
if(!chunk.isOpCode()) {
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ public class SparrowDesktop extends Application {
|
||||
}
|
||||
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
|
||||
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
|
||||
mainStage.setWidth(Config.get().getAppWidth());
|
||||
@@ -107,7 +108,7 @@ public class SparrowDesktop extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArguments(stage);
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.0";
|
||||
public static final String APP_VERSION = "1.8.3";
|
||||
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";
|
||||
|
||||
@@ -12,6 +12,7 @@ public enum UnitFormat {
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
@@ -30,6 +31,10 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableCurrencyFormat() {
|
||||
return tableCurrencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator('.');
|
||||
@@ -42,6 +47,7 @@ public enum UnitFormat {
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
@@ -60,6 +66,10 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableCurrencyFormat() {
|
||||
return tableCurrencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator(',');
|
||||
@@ -78,6 +88,8 @@ public enum UnitFormat {
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
|
||||
public abstract DecimalFormat getTableCurrencyFormat();
|
||||
|
||||
public String formatBtcValue(Long amount) {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
@@ -94,6 +106,10 @@ public enum UnitFormat {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public String tableFormatCurrencyValue(double amount) {
|
||||
return getTableCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.List;
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
private static final int MAX_SHOWN_ACCOUNTS = 8;
|
||||
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
private boolean discoverAccounts = false;
|
||||
|
||||
@@ -55,7 +57,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount) && availableAccounts.size() <= MAX_SHOWN_ACCOUNTS) {
|
||||
availableAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
|
||||
|
||||
if(transactionEntry.isConfirming()) {
|
||||
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
||||
@@ -119,6 +119,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
private static final class CoinTooltip extends Tooltip {
|
||||
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
|
||||
private boolean showConfirmations;
|
||||
private boolean isCoinbase;
|
||||
private String value;
|
||||
|
||||
public void setValue(String value) {
|
||||
@@ -126,8 +127,9 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty, boolean coinbase) {
|
||||
showConfirmations = true;
|
||||
isCoinbase = coinbase;
|
||||
|
||||
int confirmations = txEntryConfirmationsProperty.get();
|
||||
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
@@ -155,7 +157,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
if(confirmations == 0) {
|
||||
return "Unconfirmed in mempool";
|
||||
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s") + (isCoinbase ? ", immature coinbase" : "");
|
||||
} else {
|
||||
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@ import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
|
||||
public CopyableCoinLabel() {
|
||||
this("Unknown");
|
||||
}
|
||||
@@ -23,6 +27,21 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if(bitcoinUnit == null) {
|
||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
}
|
||||
|
||||
if(bitcoinUnit == BitcoinUnit.SATOSHIS) {
|
||||
bitcoinUnit = BitcoinUnit.BTC;
|
||||
} else {
|
||||
bitcoinUnit = BitcoinUnit.SATOSHIS;
|
||||
}
|
||||
|
||||
refresh(Config.get().getUnitFormat(), bitcoinUnit);
|
||||
});
|
||||
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
@@ -63,6 +82,8 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
this.bitcoinUnit = unit;
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue);
|
||||
setText(btcValue);
|
||||
|
||||
@@ -91,6 +91,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(360);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
||||
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
||||
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
|
||||
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.net.VersionCheckService;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Dragboard;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppController.DRAG_OVER_CLASS;
|
||||
|
||||
public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class);
|
||||
|
||||
private static final DateFormat signatureDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z");
|
||||
|
||||
private static final long MAX_VALID_MANIFEST_SIZE = 100 * 1024;
|
||||
private static final String SHA256SUMS_MANIFEST_PREFIX = "sha256sums";
|
||||
|
||||
private static final List<String> SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg");
|
||||
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
||||
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
||||
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
||||
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
|
||||
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
||||
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
||||
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
||||
|
||||
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
||||
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
|
||||
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
||||
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
private final ObjectProperty<File> signature = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||
|
||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||
|
||||
private final Label signedBy;
|
||||
private final Label releaseHash;
|
||||
private final Label releaseVerified;
|
||||
private final Hyperlink releaseLink;
|
||||
|
||||
private static File lastFileParent;
|
||||
|
||||
public DownloadVerifierDialog(File initialSignatureFile) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeader(new Header());
|
||||
setupDrag(dialogPane);
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
vBox.setPadding(new Insets(20, 10, 10, 20));
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset filesFieldset = new Fieldset();
|
||||
filesFieldset.setText("Files");
|
||||
filesFieldset.setSpacing(10);
|
||||
|
||||
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 publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
||||
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
||||
|
||||
filesFieldset.getChildren().addAll(signatureField, manifestField, publicKeyField, releaseFileField);
|
||||
form.getChildren().add(filesFieldset);
|
||||
|
||||
Fieldset resultsFieldset = new Fieldset();
|
||||
resultsFieldset.setText("Results");
|
||||
resultsFieldset.setSpacing(10);
|
||||
|
||||
signedBy = new Label();
|
||||
Field signedByField = setupResultField(signedBy, "Signed By");
|
||||
|
||||
releaseHash = new Label();
|
||||
Field hashMatchedField = setupResultField(releaseHash, "Release Hash");
|
||||
|
||||
releaseVerified = new Label();
|
||||
Field releaseVerifiedField = setupResultField(releaseVerified, "Verified");
|
||||
|
||||
releaseLink = new Hyperlink("");
|
||||
releaseVerifiedField.getInputs().add(releaseLink);
|
||||
releaseLink.setOnAction(event -> {
|
||||
if(release.get() != null && release.get().exists()) {
|
||||
if(release.get().getName().toLowerCase(Locale.ROOT).startsWith("sparrow")) {
|
||||
Optional<ButtonType> optType = AppServices.showAlertDialog("Exit Sparrow?", "Sparrow must be closed before installation. Exit?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
javafx.application.Platform.exit();
|
||||
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resultsFieldset.getChildren().addAll(signedByField, hashMatchedField, releaseVerifiedField);
|
||||
form.getChildren().add(resultsFieldset);
|
||||
|
||||
vBox.getChildren().addAll(form);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(clearButtonType, closeButtonType);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
if(ButtonBar.ButtonData.CANCEL_CLOSE.equals(getResult())) {
|
||||
signature.set(null);
|
||||
manifest.set(null);
|
||||
publicKey.set(null);
|
||||
release.set(null);
|
||||
signedBy.setText("");
|
||||
signedBy.setGraphic(null);
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
setResultConverter(ButtonType::getButtonData);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
dialogPane.setPrefWidth(900);
|
||||
setResizable(true);
|
||||
|
||||
signature.addListener((observable, oldValue, signatureFile) -> {
|
||||
if(signatureFile != null) {
|
||||
boolean verify = true;
|
||||
File actualSignatureFile = findSignatureFile(signatureFile);
|
||||
if(actualSignatureFile != null && !actualSignatureFile.equals(signature.get())) {
|
||||
signature.set(actualSignatureFile);
|
||||
verify = false;
|
||||
} else if(PGPUtils.signatureContainsManifest(signatureFile)) {
|
||||
manifest.set(signatureFile);
|
||||
verify = false;
|
||||
} else {
|
||||
File manifestFile = findManifestFile(signatureFile);
|
||||
if(manifestFile != null && !manifestFile.equals(manifest.get())) {
|
||||
manifest.set(manifestFile);
|
||||
verify = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(verify) {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
manifest.addListener((observable, oldValue, manifestFile) -> {
|
||||
if(manifestFile != null) {
|
||||
boolean verify = true;
|
||||
try {
|
||||
Map<File, String> manifestMap = getManifest(manifestFile);
|
||||
File releaseFile = findReleaseFile(manifestFile, manifestMap);
|
||||
if(releaseFile != null && !releaseFile.equals(release.get())) {
|
||||
release.set(releaseFile);
|
||||
verify = false;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.debug("Error reading manifest file", e);
|
||||
verify = false;
|
||||
} catch(InvalidManifestException e) {
|
||||
release.set(manifestFile);
|
||||
verify = false;
|
||||
}
|
||||
|
||||
if(verify) {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
publicKey.addListener((observable, oldValue, newValue) -> {
|
||||
verify();
|
||||
});
|
||||
|
||||
release.addListener((observable, oldValue, releaseFile) -> {
|
||||
verify();
|
||||
});
|
||||
|
||||
if(initialSignatureFile != null) {
|
||||
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrag(DialogPane dialogPane) {
|
||||
dialogPane.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != dialogPane && event.getDragboard().hasFiles()) {
|
||||
event.acceptTransferModes(TransferMode.LINK);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
dialogPane.setOnDragDropped(event -> {
|
||||
Dragboard db = event.getDragboard();
|
||||
boolean success = false;
|
||||
if(db.hasFiles()) {
|
||||
for(File file : db.getFiles()) {
|
||||
if(isVerifyDownloadFile(file)) {
|
||||
signature.set(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
event.setDropCompleted(success);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
dialogPane.setOnDragEntered(event -> {
|
||||
dialogPane.getStyleClass().add(DRAG_OVER_CLASS);
|
||||
});
|
||||
|
||||
dialogPane.setOnDragExited(event -> {
|
||||
dialogPane.getStyleClass().removeAll(DRAG_OVER_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
private void verify() {
|
||||
publicKeyDisabled.set(false);
|
||||
|
||||
if(signature.get() == null || manifest.get() == null) {
|
||||
clearReleaseFields();
|
||||
return;
|
||||
}
|
||||
|
||||
PGPVerifyService pgpVerifyService = new PGPVerifyService(signature.get(), manifest.get(), publicKey.get());
|
||||
pgpVerifyService.setOnRunning(event -> {
|
||||
signedBy.setText("Verifying...");
|
||||
signedBy.setGraphic(GlyphUtils.getBusyGlyph());
|
||||
signedBy.setTooltip(null);
|
||||
clearReleaseFields();
|
||||
});
|
||||
pgpVerifyService.setOnSucceeded(event -> {
|
||||
PGPVerificationResult result = pgpVerifyService.getValue();
|
||||
|
||||
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
||||
signedBy.setText(message);
|
||||
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
||||
|
||||
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
||||
publicKeyDisabled.set(true);
|
||||
}
|
||||
|
||||
if(manifest.get().equals(release.get())) {
|
||||
releaseHash.setText("No hash required, signature signs release file directly");
|
||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("Ready to install ");
|
||||
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseLink.setText(release.get().getName());
|
||||
} else {
|
||||
verifyManifest();
|
||||
}
|
||||
});
|
||||
pgpVerifyService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
signedBy.setText(getDisplayMessage(e));
|
||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
clearReleaseFields();
|
||||
});
|
||||
|
||||
pgpVerifyService.start();
|
||||
}
|
||||
|
||||
private void clearReleaseFields() {
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
}
|
||||
|
||||
private void verifyManifest() {
|
||||
File releaseFile = release.get();
|
||||
if(releaseFile != null && releaseFile.exists()) {
|
||||
FileSha256Service hashService = new FileSha256Service(releaseFile);
|
||||
hashService.setOnRunning(event -> {
|
||||
releaseHash.setText("Calculating...");
|
||||
releaseHash.setGraphic(GlyphUtils.getBusyGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
});
|
||||
hashService.setOnSucceeded(event -> {
|
||||
String calculatedHash = hashService.getValue();
|
||||
try {
|
||||
Map<File, String> manifestMap = getManifest(manifest.get());
|
||||
String manifestHash = getManifestHash(releaseFile.getName(), manifestMap);
|
||||
if(calculatedHash.equalsIgnoreCase(manifestHash)) {
|
||||
releaseHash.setText("Matched manifest hash");
|
||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(calculatedHash));
|
||||
releaseVerified.setText("Ready to install ");
|
||||
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseLink.setText(releaseFile.getName());
|
||||
} else if(manifestHash == null) {
|
||||
releaseHash.setText("Could not find manifest hash for " + releaseFile.getName());
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip("Manifest hashes provided for:\n" + manifestMap.keySet().stream().map(File::getName).collect(Collectors.joining("\n"))));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
} else {
|
||||
releaseHash.setText("Did not match manifest hash");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip("Calculated Hash: " + calculatedHash + "\nManifest Hash: " + manifestHash));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
} catch(IOException | InvalidManifestException e) {
|
||||
releaseHash.setText("Could not read manifest");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(e.getMessage()));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
});
|
||||
hashService.setOnFailed(event -> {
|
||||
releaseHash.setText("Could not calculate manifest");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(event.getSource().getException().getMessage()));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
});
|
||||
hashService.start();
|
||||
} else {
|
||||
releaseHash.setText("No release file");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("Not verified");
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private Field setupField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
|
||||
Field field = new Field();
|
||||
field.setText(title + ":");
|
||||
FileField fileField = new FileField(fileProperty, title, extensions, optional, example, disabledProperty);
|
||||
field.getInputs().add(fileField);
|
||||
return field;
|
||||
}
|
||||
|
||||
private Field setupResultField(Label label, String title) {
|
||||
Field field = new Field();
|
||||
field.setText(title + ":");
|
||||
field.getInputs().add(label);
|
||||
label.setGraphicTextGap(8);
|
||||
return field;
|
||||
}
|
||||
|
||||
public static Map<File, String> getManifest(File manifest) throws IOException, InvalidManifestException {
|
||||
if(manifest.length() > MAX_VALID_MANIFEST_SIZE) {
|
||||
throw new InvalidManifestException();
|
||||
}
|
||||
|
||||
try(InputStream manifestStream = new FileInputStream(manifest)) {
|
||||
return getManifest(manifestStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<File, String> getManifest(InputStream manifestStream) throws IOException {
|
||||
Map<File, String> manifest = new HashMap<>();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(manifestStream, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
String[] parts = line.split("\\s+");
|
||||
if(parts.length > 1 && parts[0].length() == 64) {
|
||||
String manifestHash = parts[0];
|
||||
String manifestFileName = parts[1];
|
||||
if(manifestFileName.startsWith("*") || manifestFileName.startsWith("U") || manifestFileName.startsWith("^")) {
|
||||
manifestFileName = manifestFileName.substring(1);
|
||||
}
|
||||
manifest.put(new File(manifestFileName), manifestHash);
|
||||
}
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private String getManifestHash(String contentFileName, Map<File, String> manifest) {
|
||||
for(Map.Entry<File, String> entry : manifest.entrySet()) {
|
||||
if(contentFileName.equalsIgnoreCase(entry.getKey().getName())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private File findSignatureFile(File providedFile) {
|
||||
for(String extension : SIGNATURE_EXTENSIONS) {
|
||||
File signatureFile = new File(providedFile.getParentFile(), providedFile.getName() + "." + extension);
|
||||
if(signatureFile.exists()) {
|
||||
return signatureFile;
|
||||
}
|
||||
}
|
||||
|
||||
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
||||
if(matcher.find()) {
|
||||
String version = matcher.group();
|
||||
File signatureFile = new File(providedFile.getParentFile(), SPARROW_RELEASE_PREFIX + version + SPARROW_SIGNATURE_SUFFIX);
|
||||
if(signatureFile.exists()) {
|
||||
return signatureFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private File findManifestFile(File providedFile) {
|
||||
String signatureName = providedFile.getName();
|
||||
if(signatureName.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> signatureName.toLowerCase(Locale.ROOT).endsWith("." + ext))) {
|
||||
File manifestFile = new File(providedFile.getParent(), signatureName.substring(0, signatureName.length() - 4));
|
||||
if(manifestFile.exists()) {
|
||||
return manifestFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
||||
List<String> releaseExtensions = getReleaseFileExtensions();
|
||||
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
||||
|
||||
for(List<String> extensions : extensionLists) {
|
||||
for(File file : manifestMap.keySet()) {
|
||||
if(extensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
|
||||
File releaseFile = new File(manifestFile.getParent(), file.getName());
|
||||
if(releaseFile.exists()) {
|
||||
return releaseFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> getReleaseFileExtensions() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
return MACOS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
return WINDOWS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
default -> {
|
||||
return LINUX_RELEASE_EXTENSIONS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getReleaseFileExample(String version) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
String arch = System.getProperty("os.arch");
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
return "Sparrow-" + version + "-" + arch;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
return "Sparrow-" + version;
|
||||
}
|
||||
default -> {
|
||||
return "sparrow_" + version + "-1_" + (arch.equals("aarch64") ? "arm64" : arch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisplayMessage(Throwable e) {
|
||||
String message = e.getMessage();
|
||||
message = message.substring(0, 1).toUpperCase(Locale.ROOT) + message.substring(1);
|
||||
|
||||
if(message.endsWith(".")) {
|
||||
message = message.substring(0, message.length() - 1);
|
||||
}
|
||||
|
||||
if(message.equals("Invalid header encountered")) {
|
||||
message += ", not a valid signature file";
|
||||
}
|
||||
|
||||
if(message.startsWith("Malformed message")) {
|
||||
message = "Not a valid signature file";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public static boolean isVerifyDownloadFile(File file) {
|
||||
if(file != null) {
|
||||
String name = file.getName().toLowerCase(Locale.ROOT);
|
||||
if(name.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(MANIFEST_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext)) || name.startsWith(SHA256SUMS_MANIFEST_PREFIX)) {
|
||||
try {
|
||||
Map<File, String> manifest = getManifest(file);
|
||||
return !manifest.isEmpty();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
||||
return matcher.find();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setSignatureFile(File signatureFile) {
|
||||
signature.set(signatureFile);
|
||||
}
|
||||
|
||||
private static class Header extends GridPane {
|
||||
public Header() {
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
getStyleClass().add("header-panel");
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setPadding(new Insets(10, 0, 0, 0));
|
||||
|
||||
Label headerLabel = new Label("Verify Download");
|
||||
headerLabel.setWrapText(true);
|
||||
headerLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
headerLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
headerLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
|
||||
CopyableLabel descriptionLabel = new CopyableLabel("Download the release file, GPG signature and optional manifest of a project to verify the download integrity");
|
||||
descriptionLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
||||
add(vBox, 0, 0);
|
||||
|
||||
StackPane graphicContainer = new StackPane();
|
||||
graphicContainer.getStyleClass().add("graphic-container");
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if (!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
graphicContainer.getChildren().add(imageView);
|
||||
}
|
||||
add(graphicContainer, 1, 0);
|
||||
|
||||
ColumnConstraints textColumn = new ColumnConstraints();
|
||||
textColumn.setFillWidth(true);
|
||||
textColumn.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints graphicColumn = new ColumnConstraints();
|
||||
graphicColumn.setFillWidth(false);
|
||||
graphicColumn.setHgrow(Priority.NEVER);
|
||||
getColumnConstraints().setAll(textColumn , graphicColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileField extends HBox {
|
||||
private final ObjectProperty<File> fileProperty;
|
||||
|
||||
public FileField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
|
||||
super(10);
|
||||
this.fileProperty = fileProperty;
|
||||
TextField textField = new TextField();
|
||||
textField.setEditable(false);
|
||||
textField.setPromptText("e.g. " + example + formatExtensionsList(extensions) + (optional ? " (optional)" : ""));
|
||||
textField.setOnMouseClicked(event -> browseForFile(title, extensions));
|
||||
Button browseButton = new Button("Browse...");
|
||||
browseButton.setOnAction(event -> browseForFile(title, extensions));
|
||||
getChildren().addAll(textField, browseButton);
|
||||
HBox.setHgrow(textField, Priority.ALWAYS);
|
||||
|
||||
fileProperty.addListener((observable, oldValue, file) -> {
|
||||
textField.setText(file == null ? "" : file.getAbsolutePath());
|
||||
if(file != null) {
|
||||
lastFileParent = file.getParentFile();
|
||||
}
|
||||
});
|
||||
|
||||
if(disabledProperty != null) {
|
||||
disabledProperty.addListener((observable, oldValue, disabled) -> {
|
||||
textField.setDisable(disabled);
|
||||
browseButton.setDisable(disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void browseForFile(String title, List<String> extensions) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open File");
|
||||
File userDir = new File(System.getProperty("user.home"));
|
||||
File downloadsDir = new File(userDir, "Downloads");
|
||||
fileChooser.setInitialDirectory(lastFileParent != null ? lastFileParent : (downloadsDir.exists() ? downloadsDir : userDir));
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(title + " files", extensions));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
fileProperty.set(file);
|
||||
}
|
||||
}
|
||||
|
||||
public String formatExtensionsList(List<String> items) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for(int i = 0; i < items.size(); i++) {
|
||||
result.append(".").append(items.get(i));
|
||||
|
||||
if (i < items.size() - 1) {
|
||||
result.append(", ");
|
||||
}
|
||||
|
||||
if (i == items.size() - 2) {
|
||||
result.append("or ");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class PGPVerifyService extends Service<PGPVerificationResult> {
|
||||
private final File signature;
|
||||
private final File manifest;
|
||||
private final File publicKey;
|
||||
|
||||
public PGPVerifyService(File signature, File manifest, File publicKey) {
|
||||
this.signature = signature;
|
||||
this.manifest = manifest;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<PGPVerificationResult> createTask() {
|
||||
return new Task<>() {
|
||||
protected PGPVerificationResult call() throws IOException, PGPVerificationException {
|
||||
boolean detachedSignature = !manifest.equals(signature);
|
||||
|
||||
try(InputStream publicKeyStream = publicKey == null ? null : new FileInputStream(publicKey);
|
||||
InputStream contentStream = new BufferedInputStream(new FileInputStream(manifest));
|
||||
InputStream detachedSignatureStream = detachedSignature ? new FileInputStream(signature) : null) {
|
||||
return PGPUtils.verify(publicKeyStream, contentStream, detachedSignatureStream);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileSha256Service extends Service<String> {
|
||||
private final File file;
|
||||
|
||||
public FileSha256Service(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
protected String call() throws IOException {
|
||||
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
return sha256(inputStream);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String sha256(InputStream stream) throws IOException {
|
||||
try {
|
||||
final byte[] buffer = new byte[1024 * 1024];
|
||||
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
int bytesRead = 0;
|
||||
while((bytesRead = stream.read(buffer)) >= 0) {
|
||||
if (bytesRead > 0) {
|
||||
sha256.update(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
return Utils.bytesToHex(sha256.digest());
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class InvalidManifestException extends Exception { }
|
||||
}
|
||||
@@ -352,12 +352,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||
.map(HashIndexEntry::getHashIndex)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(ourOutputs.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
|
||||
AppServices.showErrorDialog("No spendable outputs", "None of the outputs on this transaction are spendable.\n\nEnsure that the outputs are not frozen" +
|
||||
(transactionEntry.getConfirmations() <= 0 ? ", and spending unconfirmed UTXOs is allowed." : "."));
|
||||
return;
|
||||
}
|
||||
|
||||
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
|
||||
|
||||
@@ -20,11 +20,16 @@ public class FeeRangeSlider extends Slider {
|
||||
setSnapToTicks(false);
|
||||
setShowTickLabels(true);
|
||||
setShowTickMarks(true);
|
||||
setBlockIncrement(Math.log(1.02) / Math.log(2));
|
||||
|
||||
setLabelFormatter(new StringConverter<Double>() {
|
||||
setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
return Long.toString(FEE_RATES_RANGE.get(object.intValue()));
|
||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||
if(isLongFeeRange() && feeRate >= 1000) {
|
||||
return feeRate / 1000 + "k";
|
||||
}
|
||||
return Long.toString(feeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,6 +39,12 @@ public class FeeRangeSlider extends Slider {
|
||||
});
|
||||
|
||||
updateTrackHighlight();
|
||||
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
updateMaxFeeRange(newValue.doubleValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
@@ -41,7 +52,23 @@ public class FeeRangeSlider extends Slider {
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate) {
|
||||
setValue(Math.log(feeRate) / Math.log(2));
|
||||
double value = Math.log(feeRate) / Math.log(2);
|
||||
updateMaxFeeRange(value);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
private void updateMaxFeeRange(double value) {
|
||||
if(value >= getMax() && !isLongFeeRange()) {
|
||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
updateTrackHighlight();
|
||||
} else if(value == getMin() && isLongFeeRange()) {
|
||||
setMax(FEE_RATES_RANGE.size() - 1);
|
||||
updateTrackHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLongFeeRange() {
|
||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||
}
|
||||
|
||||
public void updateTrackHighlight() {
|
||||
@@ -75,7 +102,7 @@ public class FeeRangeSlider extends Slider {
|
||||
private Map<Integer, Double> getTargetBlocksFeeRates() {
|
||||
Map<Integer, Double> retrievedFeeRates = AppServices.getTargetBlockFeeRates();
|
||||
if(retrievedFeeRates == null) {
|
||||
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> FALLBACK_FEE_RATE,
|
||||
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> getFallbackFeeRate(),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
@@ -97,6 +124,9 @@ public class FeeRangeSlider extends Slider {
|
||||
|
||||
private int getPercentageOfFeeRange(Double feeRate) {
|
||||
double index = Math.log(feeRate) / Math.log(2);
|
||||
if(isLongFeeRange()) {
|
||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||
}
|
||||
return (int)Math.round(index * 10.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ 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.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
} else {
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof Bip129) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
|
||||
UR ur = UR.fromBytes(baos.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -10,6 +14,10 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -24,6 +32,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final WalletExport exporter;
|
||||
@@ -115,16 +125,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
String walletPassword = password.get().asString();
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
||||
try {
|
||||
exportWallet(file, decryptedWallet);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
exportWallet(file, decryptedWallet, walletPassword);
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
@@ -134,25 +140,42 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
exportWallet(file, wallet);
|
||||
exportWallet(file, wallet, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportWallet(File file, Wallet exportWallet) {
|
||||
private void exportWallet(File file, Wallet exportWallet, String password) {
|
||||
try {
|
||||
if(file != null) {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
|
||||
fileWalletExportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
}
|
||||
});
|
||||
fileWalletExportService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
});
|
||||
fileWalletExportService.start();
|
||||
} else {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
exporter.exportWallet(exportWallet, outputStream, password);
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} 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);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
@@ -165,6 +188,42 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
} finally {
|
||||
if(file == null && password != null) {
|
||||
exportWallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileWalletExportService extends Service<Void> {
|
||||
private final WalletExport exporter;
|
||||
private final File file;
|
||||
private final Wallet wallet;
|
||||
private final String password;
|
||||
|
||||
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet, String password) {
|
||||
this.exporter = exporter;
|
||||
this.file = file;
|
||||
this.wallet = wallet;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(wallet, outputStream, password);
|
||||
} finally {
|
||||
if(password != null) {
|
||||
wallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
|
||||
dialogPane.setPrefWidth(952);
|
||||
dialogPane.setPrefHeight(500);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
boolean isValidKey = isValidKey();
|
||||
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||
setScriptTypes(isValidKey);
|
||||
if(isValidKey) {
|
||||
setFromAddress();
|
||||
}
|
||||
@@ -289,6 +290,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void setScriptTypes(boolean isValidKey) {
|
||||
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
|
||||
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
|
||||
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
|
||||
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
|
||||
keyScriptType.getSelectionModel().select(0);
|
||||
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
|
||||
}
|
||||
}
|
||||
|
||||
private void scanPrivateKey() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
public enum QRDensity {
|
||||
NORMAL("Normal", 400),
|
||||
LOW("Low", 80);
|
||||
NORMAL("Normal", 400, 2000),
|
||||
LOW("Low", 80, 1000);
|
||||
|
||||
private final String name;
|
||||
private final int maxFragmentLength;
|
||||
private final int maxUrFragmentLength;
|
||||
private final int maxBbqrFragmentLength;
|
||||
|
||||
QRDensity(String name, int maxFragmentLength) {
|
||||
QRDensity(String name, int maxUrFragmentLength, int maxBbqrFragmentLength) {
|
||||
this.name = name;
|
||||
this.maxFragmentLength = maxFragmentLength;
|
||||
this.maxUrFragmentLength = maxUrFragmentLength;
|
||||
this.maxBbqrFragmentLength = maxBbqrFragmentLength;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getMaxFragmentLength() {
|
||||
return maxFragmentLength;
|
||||
public int getMaxUrFragmentLength() {
|
||||
return maxUrFragmentLength;
|
||||
}
|
||||
|
||||
public int getMaxBbqrFragmentLength() {
|
||||
return maxBbqrFragmentLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoding;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.Node;
|
||||
@@ -44,10 +47,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
private static final int DEFAULT_QR_SIZE = 580;
|
||||
private static final int REDUCED_QR_SIZE = 520;
|
||||
|
||||
private static final BBQREncoding DEFAULT_BBQR_ENCODING = BBQREncoding.ZLIB;
|
||||
|
||||
private final int qrSize = getQRSize();
|
||||
|
||||
private final UR ur;
|
||||
private UREncoder encoder;
|
||||
private UREncoder urEncoder;
|
||||
|
||||
private final BBQR bbqr;
|
||||
private BBQREncoder bbqrEncoder;
|
||||
private boolean useBbqrEncoding;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
@@ -62,17 +71,26 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
private static boolean initialDensityChange;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), addLegacyEncodingOption, false);
|
||||
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur) {
|
||||
this(ur, false, false);
|
||||
this(ur, null, false, false, false);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption, boolean addScanButton) {
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) {
|
||||
this.ur = ur;
|
||||
this.addLegacyEncodingOption = addLegacyEncodingOption;
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
this.bbqr = bbqr;
|
||||
this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption;
|
||||
|
||||
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
|
||||
if(bbqr != null) {
|
||||
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||
if(selectBbqrButton) {
|
||||
useBbqrEncoding = true;
|
||||
}
|
||||
}
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -85,16 +103,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
nextPart();
|
||||
if(encoder.isSinglePart()) {
|
||||
if(isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
createAnimateQRService();
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
|
||||
if(addLegacyEncodingOption) {
|
||||
if(this.addLegacyEncodingOption) {
|
||||
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||
} else {
|
||||
@@ -102,13 +120,19 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.getButtonTypes().add(densityButtonType);
|
||||
}
|
||||
|
||||
if(bbqr != null) {
|
||||
final ButtonType bbqrButtonType = new javafx.scene.control.ButtonType("Show BBQr", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||
dialogPane.getButtonTypes().add(bbqrButtonType);
|
||||
}
|
||||
|
||||
if(addScanButton) {
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + qrSize + 40);
|
||||
dialogPane.setPrefHeight(40 + qrSize + 85);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton);
|
||||
@@ -120,7 +144,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
public QRDisplayDialog(String data, boolean addScanButton) {
|
||||
this.ur = null;
|
||||
this.encoder = null;
|
||||
this.bbqr = null;
|
||||
this.urEncoder = null;
|
||||
this.bbqrEncoder = null;
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -133,16 +159,17 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
if(addScanButton) {
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + qrSize + 40);
|
||||
dialogPane.setPrefHeight(40 + qrSize + 85);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton);
|
||||
@@ -161,9 +188,22 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isSinglePart() {
|
||||
if(useBbqrEncoding) {
|
||||
return bbqrEncoder.isSinglePart();
|
||||
} else if(!useLegacyEncoding) {
|
||||
return urEncoder.isSinglePart();
|
||||
} else {
|
||||
return legacyParts.length == 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
if(!useLegacyEncoding) {
|
||||
String fragment = encoder.nextPart();
|
||||
if(useBbqrEncoding) {
|
||||
String fragment = bbqrEncoder.nextPart();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else if(!useLegacyEncoding) {
|
||||
String fragment = urEncoder.nextPart();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else {
|
||||
currentPart = legacyParts[legacyPartIndex];
|
||||
@@ -199,37 +239,23 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
this.legacyParts = legacyEncoder.encode();
|
||||
this.useLegacyEncoding = true;
|
||||
|
||||
if(legacyParts.length == 1) {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
nextPart();
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
}
|
||||
restartAnimation();
|
||||
} catch(UR.InvalidTypeException e) {
|
||||
//Can't happen
|
||||
}
|
||||
} else {
|
||||
this.useLegacyEncoding = false;
|
||||
restartAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
if(encoder.isSinglePart()) {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
}
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
restartAnimation();
|
||||
} else {
|
||||
this.useBbqrEncoding = false;
|
||||
restartAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,12 +264,28 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
nextPart();
|
||||
if(encoder.isSinglePart()) {
|
||||
if(bbqr != null) {
|
||||
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||
}
|
||||
|
||||
this.urEncoder = new UREncoder(ur, Config.get().getQrDensity().getMaxUrFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
|
||||
restartAnimation();
|
||||
}
|
||||
|
||||
private void restartAnimation() {
|
||||
if(isSinglePart()) {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
nextPart();
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +330,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(density, buttonData);
|
||||
density.setOnAction(event -> {
|
||||
if(!initialDensityChange && !encoder.isSinglePart()) {
|
||||
if(!initialDensityChange && !isSinglePart()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
initialDensityChange = true;
|
||||
@@ -304,12 +346,25 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
return density;
|
||||
}
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
Button scanButton = (Button)super.createButton(buttonType);
|
||||
scanButton.setGraphicTextGap(5);
|
||||
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
|
||||
return scanButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.BACK_PREVIOUS) {
|
||||
ToggleButton bbqr = new ToggleButton(buttonType.getText());
|
||||
bbqr.setGraphicTextGap(5);
|
||||
bbqr.setGraphic(getGlyph(FontAwesome5.Glyph.QRCODE));
|
||||
bbqr.setSelected(useBbqrEncoding);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(bbqr, buttonData);
|
||||
|
||||
bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseBbqrEncoding(newValue);
|
||||
});
|
||||
|
||||
return bbqr;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
|
||||
@@ -30,6 +30,8 @@ import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
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 javafx.application.Platform;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -63,8 +65,9 @@ import java.util.stream.IntStream;
|
||||
public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRScanDialog.class);
|
||||
|
||||
private final URDecoder decoder;
|
||||
private final LegacyURDecoder legacyDecoder;
|
||||
private final URDecoder urDecoder;
|
||||
private final LegacyURDecoder legacyUrDecoder;
|
||||
private final BBQRDecoder bbqrDecoder;
|
||||
private final WebcamService webcamService;
|
||||
private List<String> parts;
|
||||
|
||||
@@ -80,8 +83,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
this.urDecoder = new URDecoder();
|
||||
this.legacyUrDecoder = new LegacyURDecoder();
|
||||
this.bbqrDecoder = new BBQRDecoder();
|
||||
|
||||
if(Config.get().isHdCapture()) {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
@@ -167,12 +171,13 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
||||
});
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
|
||||
@@ -191,23 +196,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
||||
legacyUrDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyUrDecoder.getPercentComplete()));
|
||||
|
||||
if(legacyDecoder.isComplete()) {
|
||||
if(legacyUrDecoder.isComplete()) {
|
||||
try {
|
||||
UR ur = legacyDecoder.decode();
|
||||
UR ur = legacyUrDecoder.decode();
|
||||
result = extractResultFromUR(ur);
|
||||
} catch(Exception e) {
|
||||
result = new Result(new URException(e.getMessage()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
|
||||
urDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(urDecoder.getProcessedPartsCount() > 0 ? urDecoder.getEstimatedPercentComplete() : 0));
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
if(urDecoder.getResult() != null) {
|
||||
URDecoder.Result urResult = urDecoder.getResult();
|
||||
if(urResult.type == ResultType.SUCCESS) {
|
||||
result = extractResultFromUR(urResult.ur);
|
||||
Platform.runLater(() -> setResult(result));
|
||||
@@ -216,6 +221,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(BBQRDecoder.isBBQRFragment(qrtext)) {
|
||||
bbqrDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(bbqrDecoder.getPercentComplete()));
|
||||
|
||||
if(bbqrDecoder.getResult() != null) {
|
||||
BBQRDecoder.Result bbqrResult = bbqrDecoder.getResult();
|
||||
if(bbqrResult.getResultType() == BBQRDecoder.ResultType.SUCCESS) {
|
||||
result = extractResultFromBBQR(bbqrResult);
|
||||
Platform.runLater(() -> setResult(result));
|
||||
} else {
|
||||
result = new Result(new BBQRException(bbqrResult.getError()));
|
||||
}
|
||||
}
|
||||
} else if(partMatcher.matches()) {
|
||||
int m = Integer.parseInt(partMatcher.group(1));
|
||||
int n = Integer.parseInt(partMatcher.group(2));
|
||||
@@ -441,6 +459,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(cryptoBip39);
|
||||
return new Result(seed);
|
||||
} else if(urRegistryType.equals(RegistryType.PSBT)) {
|
||||
URPSBT urPSBT = (URPSBT)ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(urPSBT.getPsbt(), false);
|
||||
return new Result(psbt);
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
|
||||
return new Result(new URException("Error parsing PSBT from UR type " + urRegistryType, e));
|
||||
}
|
||||
} else if(urRegistryType.equals(RegistryType.ADDRESS)) {
|
||||
URAddress urAddress = (URAddress)ur.decodeFromRegistry();
|
||||
Address address = getAddress(urAddress);
|
||||
if(address != null) {
|
||||
return new Result(BitcoinURI.fromAddress(address));
|
||||
} else {
|
||||
return new Result(new URException("Unknown " + urRegistryType + " type of " + urAddress.getType()));
|
||||
}
|
||||
} else if(urRegistryType.equals(RegistryType.HDKEY)) {
|
||||
URHDKey urHDKey = (URHDKey)ur.decodeFromRegistry();
|
||||
ExtendedKey extendedKey = getExtendedKey(urHDKey);
|
||||
return new Result(extendedKey, urHDKey.getName());
|
||||
} else if(urRegistryType.equals(RegistryType.OUTPUT_DESCRIPTOR)) {
|
||||
UROutputDescriptor urOutputDescriptor = (UROutputDescriptor)ur.decodeFromRegistry();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
|
||||
return new Result(outputDescriptor);
|
||||
} else if(urRegistryType.equals(RegistryType.ACCOUNT_DESCRIPTOR)) {
|
||||
URAccountDescriptor urAccountDescriptor = (URAccountDescriptor)ur.decodeFromRegistry();
|
||||
List<Wallet> wallets = getWallets(urAccountDescriptor);
|
||||
return new Result(wallets);
|
||||
} else if(urRegistryType.equals(RegistryType.SEED)) {
|
||||
URSeed urSeed = (URSeed)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(urSeed);
|
||||
return new Result(seed);
|
||||
} else {
|
||||
log.error("Unsupported UR type " + urRegistryType);
|
||||
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
|
||||
@@ -583,6 +634,53 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
|
||||
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
|
||||
private OutputDescriptor getOutputDescriptor(UROutputDescriptor urOutputDescriptor) {
|
||||
String source = urOutputDescriptor.getSource();
|
||||
List<RegistryItem> keys = urOutputDescriptor.getKeys();
|
||||
Map<ExtendedKey, String> mapExtendedPublicKeyLabels = new LinkedHashMap<>();
|
||||
|
||||
for(int i = 0; i < keys.size(); i++) {
|
||||
RegistryItem key = keys.get(i);
|
||||
if(key instanceof URHDKey urhdKey) {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
|
||||
}
|
||||
}
|
||||
|
||||
return OutputDescriptor.getOutputDescriptor(source, mapExtendedPublicKeyLabels);
|
||||
}
|
||||
|
||||
private List<Wallet> getWallets(URAccountDescriptor urAccountDescriptor) {
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
String masterFingerprint = Utils.bytesToHex(urAccountDescriptor.getMasterFingerprint());
|
||||
for(UROutputDescriptor urOutputDescriptor : urAccountDescriptor.getOutputDescriptors()) {
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(urOutputDescriptor);
|
||||
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
return wallets;
|
||||
}
|
||||
|
||||
private Result extractResultFromBBQR(BBQRDecoder.Result result) {
|
||||
if(result.getPsbt() != null) {
|
||||
return new Result(result.getPsbt());
|
||||
} else if(result.getTransaction() != null) {
|
||||
return new Result(result.getTransaction());
|
||||
} else if(result.toString() != null) {
|
||||
return new Result(result.toString());
|
||||
} else {
|
||||
log.error("Unsupported BBQR type " + result.getBbqrType());
|
||||
return new Result(new URException("BBQR type " + result.getBbqrType() + " is not supported"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanListener implements WebcamListener {
|
||||
|
||||
@@ -58,6 +58,8 @@ public class ScriptArea extends CodeArea {
|
||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isTaprootControlBlock()) {
|
||||
append("<controlblock>", "script-controlblock");
|
||||
} else if(chunk.isString()) {
|
||||
append(chunk.toString(), "script-other");
|
||||
} else if(chunk.isScript()) {
|
||||
|
||||
@@ -2,25 +2,22 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.sparrow.BaseController;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
import org.fxmisc.richtext.model.TwoDimensional;
|
||||
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
|
||||
|
||||
public class ScriptContextMenu extends ContextMenu {
|
||||
private Script script;
|
||||
private MenuItem copyvalue;
|
||||
private ScriptChunk hoverChunk;
|
||||
|
||||
public ScriptContextMenu(CodeArea area, Script script)
|
||||
public ScriptContextMenu(ScriptArea area, Script script)
|
||||
{
|
||||
this.script = script;
|
||||
|
||||
@@ -40,12 +37,9 @@ public class ScriptContextMenu extends ContextMenu {
|
||||
Point2D point = area.screenToLocal(event.getScreenX(), event.getScreenY());
|
||||
OptionalInt characterIndex = area.hit(point.getX(), point.getY()).getCharacterIndex();
|
||||
if(characterIndex.isPresent()) {
|
||||
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(characterIndex.getAsInt(), Backward);
|
||||
if(position.getMajor() % 2 == 0) {
|
||||
ScriptChunk chunk = script.getChunks().get(position.getMajor() / 2);
|
||||
if(!chunk.isOpCode()) {
|
||||
this.hoverChunk = chunk;
|
||||
}
|
||||
ScriptChunk chunk = BaseController.getScriptChunk(area, characterIndex.getAsInt());
|
||||
if(chunk != null) {
|
||||
this.hoverChunk = chunk;
|
||||
}
|
||||
}
|
||||
copyvalue.setDisable(hoverChunk == null);
|
||||
|
||||
@@ -81,6 +81,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
setResizable(true);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
|
||||
dialogPane.setPrefWidth(700);
|
||||
dialogPane.setPrefHeight(400);
|
||||
setResizable(true);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
@@ -44,7 +45,6 @@ import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
@@ -56,6 +56,8 @@ import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.glyphfont.GlyphUtils.*;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
@@ -89,7 +91,7 @@ public class TransactionDiagram extends GridPane {
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS || Platform.getCurrent() == Platform.UNIX) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
@@ -474,7 +476,7 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph());
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
@@ -642,7 +644,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().filter(txo -> txo.getScript().getToAddress() != null).map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
@@ -686,19 +688,18 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
List<OutputNode> outputNodes = new ArrayList<>();
|
||||
for(Payment payment : displayedPayments) {
|
||||
Glyph outputGlyph = getOutputGlyph(payment);
|
||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString())
|
||||
+ (isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
@@ -720,6 +721,7 @@ public class TransactionDiagram extends GridPane {
|
||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
|
||||
}
|
||||
|
||||
Set<Integer> seenIndexes = new HashSet<>();
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
WalletNode changeNode = changeEntry.getKey();
|
||||
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
||||
@@ -765,11 +767,15 @@ public class TransactionDiagram extends GridPane {
|
||||
actionBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
if(isFinal()) {
|
||||
Collections.sort(outputNodes);
|
||||
int changeIndex = outputNodes.size();
|
||||
if(isFinal()) {
|
||||
changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes);
|
||||
seenIndexes.add(changeIndex);
|
||||
if(changeIndex > outputNodes.size()) {
|
||||
changeIndex = outputNodes.size();
|
||||
}
|
||||
}
|
||||
outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
@@ -783,7 +789,7 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
@@ -923,38 +929,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private String getOutputLabel(Payment payment) {
|
||||
if(payment.getLabel() != null) {
|
||||
return payment.getLabel();
|
||||
}
|
||||
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool fee";
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return "Badbank change";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address, long amount) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
Wallet getToWallet(Payment payment) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
|
||||
List<TransactionOutput> addressOutputs = walletTx.getTransaction().getOutputs().stream().filter(txOutput -> txOutput.getScript().getToAddress() != null).collect(Collectors.toList());
|
||||
TransactionOutput output = addressOutputs.stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex())).findFirst().orElseThrow();
|
||||
return addressOutputs.indexOf(output);
|
||||
}
|
||||
|
||||
private Wallet getBip47SendWallet(Payment payment) {
|
||||
@@ -974,164 +952,6 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(Payment payment) {
|
||||
if(payment.getType().equals(Payment.Type.MIX)) {
|
||||
return getMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
return getFakeMixGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return getBadbankGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
|
||||
return getWhirlpoolFeeGlyph();
|
||||
} else if(payment instanceof AdditionalPayment) {
|
||||
return ((AdditionalPayment)payment).getOutputGlyph(this);
|
||||
} else if(getToWallet(payment) != null) {
|
||||
return getDepositGlyph();
|
||||
} else if(isDuplicateAddress(payment)) {
|
||||
return getPaymentWarningGlyph();
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
private boolean isDuplicateAddress(Payment payment) {
|
||||
return walletTx.getPayments().stream().filter(p -> payment != p).anyMatch(p -> payment.getAddress() != null && payment.getAddress().equals(p.getAddress()));
|
||||
}
|
||||
|
||||
public static Glyph getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
excludeGlyph.setFontSize(12);
|
||||
return excludeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPaymentGlyph() {
|
||||
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
paymentGlyph.getStyleClass().add("payment-icon");
|
||||
paymentGlyph.setFontSize(12);
|
||||
return paymentGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPaymentWarningGlyph() {
|
||||
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
|
||||
paymentWarningGlyph.setFontSize(12);
|
||||
return paymentWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getConsolidationGlyph() {
|
||||
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
|
||||
consolidationGlyph.getStyleClass().add("consolidation-icon");
|
||||
consolidationGlyph.setFontSize(12);
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDepositGlyph() {
|
||||
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
depositGlyph.getStyleClass().add("deposit-icon");
|
||||
depositGlyph.setFontSize(12);
|
||||
return depositGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPremixGlyph() {
|
||||
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
premixGlyph.getStyleClass().add("premix-icon");
|
||||
premixGlyph.setFontSize(12);
|
||||
return premixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getBadbankGlyph() {
|
||||
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
|
||||
badbankGlyph.getStyleClass().add("badbank-icon");
|
||||
badbankGlyph.setFontSize(12);
|
||||
return badbankGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWhirlpoolFeeGlyph() {
|
||||
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
|
||||
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
|
||||
whirlpoolFeeGlyph.setFontSize(12);
|
||||
return whirlpoolFeeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFakeMixGlyph() {
|
||||
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
|
||||
fakeMixGlyph.getStyleClass().add("fakemix-icon");
|
||||
fakeMixGlyph.setFontSize(12);
|
||||
return fakeMixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getTxoGlyph() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getMixGlyph() {
|
||||
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
payjoinGlyph.getStyleClass().add("mix-icon");
|
||||
payjoinGlyph.setFontSize(12);
|
||||
return payjoinGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeGlyph() {
|
||||
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
changeGlyph.getStyleClass().add("change-icon");
|
||||
changeGlyph.setFontSize(12);
|
||||
return changeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeWarningGlyph() {
|
||||
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
changeWarningGlyph.getStyleClass().add("change-warning-icon");
|
||||
changeWarningGlyph.setFontSize(12);
|
||||
return changeWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeReplaceGlyph() {
|
||||
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
|
||||
changeReplaceGlyph.setFontSize(12);
|
||||
return changeReplaceGlyph;
|
||||
}
|
||||
|
||||
public Glyph getFeeGlyph() {
|
||||
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
|
||||
feeGlyph.getStyleClass().add("fee-icon");
|
||||
feeGlyph.setFontSize(12);
|
||||
return feeGlyph;
|
||||
}
|
||||
|
||||
private Glyph getWarningGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private Glyph getQuestionGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("question-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private Glyph getLockGlyph() {
|
||||
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
|
||||
lockGlyph.getStyleClass().add("lock-icon");
|
||||
lockGlyph.setFontSize(12);
|
||||
return lockGlyph;
|
||||
}
|
||||
|
||||
private Glyph getUserGlyph() {
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
|
||||
userGlyph.getStyleClass().add("user-icon");
|
||||
userGlyph.setFontSize(12);
|
||||
return userGlyph;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
@@ -1288,7 +1108,7 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AdditionalPayment extends Payment {
|
||||
public static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
public AdditionalPayment(List<Payment> additionalPayments) {
|
||||
@@ -1296,10 +1116,10 @@ public class TransactionDiagram extends GridPane {
|
||||
this.additionalPayments = additionalPayments;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
|
||||
public Glyph getOutputGlyph(WalletTransaction walletTx) {
|
||||
Glyph glyph = null;
|
||||
for(Payment payment : additionalPayments) {
|
||||
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
|
||||
Glyph paymentGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
@@ -1315,7 +1135,7 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
private static class OutputNode {
|
||||
public Pane outputLabel;
|
||||
public Address address;
|
||||
public long amount;
|
||||
@@ -1325,15 +1145,6 @@ public class TransactionDiagram extends GridPane {
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||
try {
|
||||
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
|
||||
} catch(Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelContextMenu extends ContextMenu {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -81,13 +83,13 @@ public class TransactionDiagramLabel extends HBox {
|
||||
List<Payment> badbankOutputs = walletTx.getPayments().stream().filter(walletTx::isBadbankSend).collect(Collectors.toList());
|
||||
List<OutputLabel> badbankOutputLabels = badbankOutputs.stream().map(payment -> getBadbankOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
|
||||
outputLabels.addAll(badbankOutputLabels);
|
||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
|
||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_PREMIX && walletTx.getPayments().stream().anyMatch(walletTx::isPostmixSend)) {
|
||||
OutputLabel mixOutputLabel = getMixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||
if(mixOutputLabel != null) {
|
||||
outputLabels.add(mixOutputLabel);
|
||||
}
|
||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1
|
||||
} else if(walletTx.getPayments().size() >= 5 && walletTx.getPayments().stream().mapToLong(Payment::getAmount).distinct().count() <= 1 && walletTx.getWallet() != null
|
||||
&& walletTx.getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && walletTx.getPayments().stream().anyMatch(walletTx::isConsolidationSend)) {
|
||||
OutputLabel remixOutputLabel = getRemixOutputLabel(transactionDiagram, walletTx.getPayments());
|
||||
if(remixOutputLabel != null) {
|
||||
@@ -142,7 +144,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
Payment premixOutput = premixOutputs.get(0);
|
||||
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
Glyph glyph = transactionDiagram.getOutputGlyph(premixOutput);
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), premixOutput);
|
||||
String text;
|
||||
if(premixOutputs.size() == 1) {
|
||||
text = "Premix transaction with 1 output of " + transactionDiagram.getSatsValue(premixOutput.getAmount()) + " sats";
|
||||
@@ -155,7 +157,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
}
|
||||
|
||||
private OutputLabel getBadbankOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||
String text = "Badbank change of " + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
@@ -164,7 +166,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
private OutputLabel getWhirlpoolFeeOutputLabel(TransactionDiagram transactionDiagram, Payment whirlpoolFee, List<Payment> premixOutputs) {
|
||||
long total = premixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
double feePercentage = (double)whirlpoolFee.getAmount() / (total - whirlpoolFee.getAmount());
|
||||
Glyph glyph = transactionDiagram.getOutputGlyph(whirlpoolFee);
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), whirlpoolFee);
|
||||
String text = "Whirlpool fee of " + transactionDiagram.getSatsValue(whirlpoolFee.getAmount()) + " sats (" + String.format("%.2f", feePercentage * 100.0) + "% of total premix value)";
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
@@ -177,7 +179,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
Payment remixOutput = mixOutputs.get(0);
|
||||
long total = mixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
Glyph glyph = TransactionDiagram.getPremixGlyph();
|
||||
Glyph glyph = GlyphUtils.getPremixGlyph();
|
||||
String text = "Mix transaction with " + mixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
|
||||
+ transactionDiagram.getSatsValue(total) + " sats)";
|
||||
|
||||
@@ -191,7 +193,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
Payment remixOutput = remixOutputs.get(0);
|
||||
long total = remixOutputs.stream().mapToLong(Payment::getAmount).sum();
|
||||
Glyph glyph = TransactionDiagram.getPremixGlyph();
|
||||
Glyph glyph = GlyphUtils.getPremixGlyph();
|
||||
String text = "Remix transaction with " + remixOutputs.size() + " outputs of " + transactionDiagram.getSatsValue(remixOutput.getAmount()) + " sats each ("
|
||||
+ transactionDiagram.getSatsValue(total) + " sats)";
|
||||
|
||||
@@ -200,10 +202,10 @@ public class TransactionDiagramLabel extends HBox {
|
||||
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
|
||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||
Wallet toWallet = transactionDiagram.getToWallet(payment);
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
|
||||
Glyph glyph = transactionDiagram.getOutputGlyph(payment);
|
||||
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
|
||||
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getSatsValue(payment.getAmount()) + " sats to " + payment.getAddress().toString();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
@@ -212,7 +214,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
|
||||
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
|
||||
|
||||
Glyph glyph = TransactionDiagram.getChangeGlyph();
|
||||
Glyph glyph = GlyphUtils.getChangeGlyph();
|
||||
String text = "Change of " + transactionDiagram.getSatsValue(changeEntry.getValue()) + " sats to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
@@ -224,7 +226,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
return null;
|
||||
}
|
||||
|
||||
Glyph glyph = transactionDiagram.getFeeGlyph();
|
||||
Glyph glyph = GlyphUtils.getFeeGlyph();
|
||||
String text = "Fee of " + transactionDiagram.getSatsValue(walletTx.getFee()) + " sats (" + String.format("%.2f", walletTx.getFeePercentage() * 100.0) + "%)";
|
||||
|
||||
return getOutputLabel(glyph, text);
|
||||
|
||||
@@ -27,7 +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.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.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 -> {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@@ -17,7 +18,9 @@ import java.util.List;
|
||||
public class WalletExportDialog extends Dialog<Wallet> {
|
||||
private Wallet wallet;
|
||||
|
||||
public WalletExportDialog(Wallet wallet) {
|
||||
public WalletExportDialog(WalletForm walletForm) {
|
||||
this.wallet = walletForm.getWallet();
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -42,9 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels());
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels());
|
||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
||||
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
@@ -64,6 +68,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(480);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
|
||||
@@ -51,7 +51,7 @@ 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());
|
||||
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());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
@@ -90,6 +90,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(600);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
|
||||
@@ -116,6 +116,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
ButtonType okButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
|
||||
setResizable(true);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.google.zxing.*;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
import com.sparrowwallet.bokmakierie.Bokmakierie;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -43,6 +44,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
private Webcam cam;
|
||||
private long lastQrSampleTime;
|
||||
private final Reader qrReader;
|
||||
private final Bokmakierie bokmakierie;
|
||||
|
||||
static {
|
||||
Webcam.setDriver(new WebcamScanDriver());
|
||||
@@ -55,6 +57,7 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
this.delayCalculator = delayCalculator;
|
||||
this.lastQrSampleTime = System.currentTimeMillis();
|
||||
this.qrReader = new QRCodeReader();
|
||||
this.bokmakierie = new Bokmakierie();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,6 +159,15 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
com.sparrowwallet.bokmakierie.Result result = bokmakierie.scan(bufferedImage);
|
||||
if(result != null) {
|
||||
return new Result(result.getMessage(), result.getRawBytes(), new ResultPoint[0], BarcodeFormat.QR_CODE);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error scanning QR", e);
|
||||
}
|
||||
|
||||
if(ZBar.isEnabled()) {
|
||||
ZBar.Scan scan = ZBar.scan(bufferedImage);
|
||||
if(scan != null) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
|
||||
public class PSBTReorderedEvent {
|
||||
private final PSBT psbt;
|
||||
|
||||
public PSBTReorderedEvent(PSBT psbt) {
|
||||
this.psbt = psbt;
|
||||
}
|
||||
|
||||
public PSBT getPsbt() {
|
||||
return psbt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class RequestVerifyDownloadEvent {
|
||||
private final Window window;
|
||||
private final File file;
|
||||
|
||||
public RequestVerifyDownloadEvent(Window window) {
|
||||
this.window = window;
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
public RequestVerifyDownloadEvent(Window window, File file) {
|
||||
this.window = window;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HAND_HOLDING_WATER('\uf4c1'),
|
||||
HOURGLASS_HALF('\uf252'),
|
||||
HISTORY('\uf1da'),
|
||||
INFO_CIRCLE('\uf05a'),
|
||||
KEY('\uf084'),
|
||||
@@ -51,8 +52,10 @@ public class FontAwesome5 extends GlyphFont {
|
||||
LINK('\uf0c1'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
LONG_ARROW_ALT_RIGHT('\uf30b'),
|
||||
MAGNIFYING_GLASS_PLUS('\uf00e'),
|
||||
MAGNIFYING_GLASS_MINUS('\uf010'),
|
||||
MICROCHIP('\uf2db'),
|
||||
MINUS_CIRCLE('\uf056'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.sparrowwallet.sparrow.glyphfont;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class GlyphUtils {
|
||||
public static Glyph getOutputGlyph(WalletTransaction walletTx, Payment payment) {
|
||||
if(payment.getType().equals(Payment.Type.MIX)) {
|
||||
return getMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
return getFakeMixGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return getBadbankGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
|
||||
return getWhirlpoolFeeGlyph();
|
||||
} else if(payment instanceof TransactionDiagram.AdditionalPayment) {
|
||||
return ((TransactionDiagram.AdditionalPayment)payment).getOutputGlyph(walletTx);
|
||||
} else if(walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment) != null) {
|
||||
return getDepositGlyph();
|
||||
} else if(walletTx.isDuplicateAddress(payment)) {
|
||||
return getPaymentWarningGlyph();
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getPaymentGlyph() {
|
||||
Glyph paymentGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
paymentGlyph.getStyleClass().add("payment-icon");
|
||||
paymentGlyph.setFontSize(12);
|
||||
return paymentGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPaymentWarningGlyph() {
|
||||
Glyph paymentWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
paymentWarningGlyph.getStyleClass().add("payment-warning-icon");
|
||||
paymentWarningGlyph.setFontSize(12);
|
||||
return paymentWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getConsolidationGlyph() {
|
||||
Glyph consolidationGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.REPLY_ALL);
|
||||
consolidationGlyph.getStyleClass().add("consolidation-icon");
|
||||
consolidationGlyph.setFontSize(12);
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDepositGlyph() {
|
||||
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
depositGlyph.getStyleClass().add("deposit-icon");
|
||||
depositGlyph.setFontSize(12);
|
||||
return depositGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPremixGlyph() {
|
||||
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
premixGlyph.getStyleClass().add("premix-icon");
|
||||
premixGlyph.setFontSize(12);
|
||||
return premixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getBadbankGlyph() {
|
||||
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
|
||||
badbankGlyph.getStyleClass().add("badbank-icon");
|
||||
badbankGlyph.setFontSize(12);
|
||||
return badbankGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWhirlpoolFeeGlyph() {
|
||||
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
|
||||
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
|
||||
whirlpoolFeeGlyph.setFontSize(12);
|
||||
return whirlpoolFeeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFakeMixGlyph() {
|
||||
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
|
||||
fakeMixGlyph.getStyleClass().add("fakemix-icon");
|
||||
fakeMixGlyph.setFontSize(12);
|
||||
return fakeMixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getTxoGlyph() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getMixGlyph() {
|
||||
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
payjoinGlyph.getStyleClass().add("mix-icon");
|
||||
payjoinGlyph.setFontSize(12);
|
||||
return payjoinGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getExternalInputGlyph() {
|
||||
Glyph externalGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LONG_ARROW_ALT_RIGHT);
|
||||
externalGlyph.getStyleClass().add("external-input-icon");
|
||||
externalGlyph.setFontSize(12);
|
||||
return externalGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
excludeGlyph.setFontSize(12);
|
||||
return excludeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeGlyph() {
|
||||
Glyph changeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
changeGlyph.getStyleClass().add("change-icon");
|
||||
changeGlyph.setFontSize(12);
|
||||
return changeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeWarningGlyph() {
|
||||
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
changeWarningGlyph.getStyleClass().add("change-warning-icon");
|
||||
changeWarningGlyph.setFontSize(12);
|
||||
return changeWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeReplaceGlyph() {
|
||||
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
|
||||
changeReplaceGlyph.setFontSize(12);
|
||||
return changeReplaceGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFeeGlyph() {
|
||||
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
|
||||
feeGlyph.getStyleClass().add("fee-icon");
|
||||
feeGlyph.setFontSize(12);
|
||||
return feeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFeeWarningGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("fee-warning-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getQuestionGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("question-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getLockGlyph() {
|
||||
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
|
||||
lockGlyph.getStyleClass().add("lock-icon");
|
||||
lockGlyph.setFontSize(12);
|
||||
return lockGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getUserGlyph() {
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
|
||||
userGlyph.getStyleClass().add("user-icon");
|
||||
userGlyph.setFontSize(12);
|
||||
return userGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getOpcodeGlyph() {
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.MICROCHIP);
|
||||
userGlyph.getStyleClass().add("opcode-icon");
|
||||
userGlyph.setFontSize(12);
|
||||
return userGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getSuccessGlyph() {
|
||||
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
successGlyph.getStyleClass().add("success");
|
||||
successGlyph.setFontSize(12);
|
||||
return successGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWarningGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
warningGlyph.setFontSize(12);
|
||||
return warningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailureGlyph() {
|
||||
Glyph failureGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
failureGlyph.getStyleClass().add("failure");
|
||||
failureGlyph.setFontSize(12);
|
||||
return failureGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getBusyGlyph() {
|
||||
Glyph busyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HOURGLASS_HALF);
|
||||
busyGlyph.getStyleClass().add("busy");
|
||||
busyGlyph.setFontSize(12);
|
||||
return busyGlyph;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public class AirGapVault extends KeystoneSinglesig {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "AirGap Vault";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your AirGap Vault, by selecting the Secret > Account > Sparrow Wallet";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.AIRGAP_VAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeprecated() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
String record = "BSMS 1.0\n" +
|
||||
OutputDescriptor.getOutputDescriptor(wallet) +
|
||||
|
||||
@@ -94,7 +94,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
if(!wallet.isValid()) {
|
||||
throw new ExportException("Cannot export an incomplete wallet");
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public abstract class CardApi {
|
||||
}
|
||||
|
||||
public static CardApi getCardApi(WalletModel walletModel, String pin) throws CardException {
|
||||
if(walletModel == WalletModel.TAPSIGNER || walletModel == WalletModel.SATSCARD) {
|
||||
if(walletModel == WalletModel.TAPSIGNER || walletModel == WalletModel.SATSCHIP || walletModel == WalletModel.SATSCARD) {
|
||||
return new CkCardApi(walletModel, pin);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
if(!wallet.isValid()) {
|
||||
throw new ExportException("Cannot export an incomplete wallet");
|
||||
}
|
||||
@@ -224,17 +224,17 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,12 +45,12 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,7 +23,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
bufferedWriter.write("# Receive and change descriptor (BIP389):");
|
||||
@@ -65,7 +65,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
@@ -18,7 +15,10 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
|
||||
@@ -301,11 +301,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "json";
|
||||
return wallet.isEncrypted() ? "" : "json";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
ElectrumJsonWallet ew = new ElectrumJsonWallet();
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
@@ -342,7 +342,18 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
ek.seed = keystore.getSeed().getMnemonicString().asString();
|
||||
ek.passphrase = keystore.getSeed().getPassphrase() == null ? null : keystore.getSeed().getPassphrase().asString();
|
||||
}
|
||||
ew.use_encryption = false;
|
||||
if(password != null) {
|
||||
ek.xprv = encrypt(ek.xprv, password);
|
||||
if(ek.seed != null) {
|
||||
ek.seed = encrypt(ek.seed, password);
|
||||
}
|
||||
if(ek.passphrase != null) {
|
||||
ek.passphrase = encrypt(ek.passphrase, password);
|
||||
}
|
||||
ew.use_encryption = true;
|
||||
} else {
|
||||
ew.use_encryption = false;
|
||||
}
|
||||
} else if(keystore.getSource() == KeystoreSource.SW_WATCH) {
|
||||
ek.type = "bip32";
|
||||
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
|
||||
@@ -373,14 +384,41 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
|
||||
gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
String json = gson.toJson(eJson);
|
||||
if(password != null) {
|
||||
ECKey encryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password);
|
||||
outputStream = new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionKey));
|
||||
}
|
||||
|
||||
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Error exporting Electrum Wallet", e);
|
||||
throw new ExportException("Error exporting Electrum Wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encrypt(String plain, String password) throws NoSuchAlgorithmException {
|
||||
if(plain == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] plainBytes = plain.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
KeyDeriver keyDeriver = new DoubleSha256KeyDeriver();
|
||||
Key key = keyDeriver.deriveKey(password);
|
||||
|
||||
KeyCrypter keyCrypter = new AESKeyCrypter();
|
||||
byte[] initializationVector = new byte[16];
|
||||
SecureRandom.getInstanceStrong().nextBytes(initializationVector);
|
||||
EncryptedData encryptedData = keyCrypter.encrypt(plainBytes, initializationVector, key);
|
||||
byte[] encrypted = new byte[initializationVector.length + encryptedData.getEncryptedBytes().length];
|
||||
System.arraycopy(initializationVector, 0, encrypted, 0, 16);
|
||||
System.arraycopy(encryptedData.getEncryptedBytes(), 0, encrypted, 16, encryptedData.getEncryptedBytes().length);
|
||||
byte[] encryptedBase64 = Base64.getEncoder().encode(encrypted);
|
||||
return new String(encryptedBase64, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return (FileType.BINARY.equals(IOUtils.getFileType(file)));
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ElectrumPersonalServer implements WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
if(wallet.getScriptType() == ScriptType.P2TR) {
|
||||
throw new ExportException(getName() + " does not support Taproot wallets.");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -24,19 +27,18 @@ import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
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.3.1";
|
||||
private static final String HWI_VERSION = "2.4.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
@@ -377,9 +379,9 @@ public class Hwi {
|
||||
if(platform == Platform.OSX) {
|
||||
InputStream inputStream;
|
||||
if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.zip");
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
@@ -396,26 +398,27 @@ public class Hwi {
|
||||
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
|
||||
|
||||
File hwiExec = null;
|
||||
try(ZipInputStream zis = new ZipInputStream(inputStream)) {
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while(zipEntry != null) {
|
||||
if(zipEntry.isDirectory()) {
|
||||
newDirectory(hwiVersionDir, zipEntry, ownerExecutableWritable);
|
||||
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
|
||||
TarArchiveEntry tarEntry = tarInput.getNextEntry();
|
||||
while(tarEntry != null) {
|
||||
if(tarEntry.isDirectory()) {
|
||||
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else if(tarEntry.isSymbolicLink()) {
|
||||
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(hwiVersionDir, zipEntry, ownerExecutableWritable);
|
||||
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
try(FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||
ByteStreams.copy(zis, new FileOutputStream(newFile));
|
||||
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
};
|
||||
}
|
||||
|
||||
if(zipEntry.getName().equals("hwi")) {
|
||||
if(tarEntry.getName().equals("hwi")) {
|
||||
hwiExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
zipEntry = zis.getNextEntry();
|
||||
tarEntry = tarInput.getNextEntry();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
|
||||
hwiExecutable = hwiExec;
|
||||
@@ -478,29 +481,43 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, zipEntry.getName());
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destSubDirPath = destDir.getCanonicalPath();
|
||||
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, zipEntry.getName());
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
}
|
||||
|
||||
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
@@ -836,7 +853,7 @@ public class Hwi {
|
||||
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
String modelStr = json.getAsJsonPrimitive().getAsString();
|
||||
try {
|
||||
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT));
|
||||
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
|
||||
} catch(Exception e) {
|
||||
for(WalletModel model : WalletModel.values()) {
|
||||
if(modelStr.startsWith(model.getType())) {
|
||||
|
||||
@@ -17,9 +17,4 @@ public class Krux extends SpecterDIY {
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.KRUX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
PersistenceType persistenceType = PersistenceType.DB;
|
||||
|
||||
@@ -65,7 +65,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
|
||||
|
||||
@@ -18,7 +18,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
SpecterWallet specterWallet = new SpecterWallet();
|
||||
specterWallet.label = wallet.getFullName();
|
||||
@@ -108,7 +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.KEEPKEY ||
|
||||
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) {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
|
||||
@@ -752,7 +752,7 @@ public class Storage {
|
||||
protected Void call() throws IOException, ExportException {
|
||||
Sparrow export = new Sparrow();
|
||||
try(BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(newWalletFile))) {
|
||||
export.exportWallet(wallet, outputStream);
|
||||
export.exportWallet(wallet, outputStream, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface WalletExport extends ImportExport {
|
||||
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
|
||||
void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException;
|
||||
String getWalletExportDescription();
|
||||
String getExportFileExtension(Wallet wallet);
|
||||
boolean isWalletExportScannable();
|
||||
|
||||
@@ -37,7 +37,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Wallet Labels";
|
||||
return "Labels";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,7 +46,7 @@ public class WalletLabels implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
List<Label> labels = new ArrayList<>();
|
||||
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
|
||||
for(Wallet exportWallet : allWallets) {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletTransactions implements WalletExport {
|
||||
private static final long ONE_DAY = 24*60*60*1000L;
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final WalletForm walletForm;
|
||||
|
||||
public WalletTransactions(WalletForm walletForm) {
|
||||
this.walletForm = walletForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Transactions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.TRANSACTIONS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
|
||||
ExchangeSource exchangeSource = Config.get().getExchangeSource();
|
||||
if(Config.get().getExchangeSource() == null) {
|
||||
exchangeSource = ExchangeSource.COINGECKO;
|
||||
}
|
||||
|
||||
Currency fiatCurrency = (exchangeSource == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency());
|
||||
Map<Date, Double> fiatRates = new HashMap<>();
|
||||
if(fiatCurrency != null && !walletTransactionsEntry.getChildren().isEmpty()) {
|
||||
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
|
||||
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.summarizingLong(Date::getTime));
|
||||
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(stats.getMin() - ONE_DAY), new Date(stats.getMax()));
|
||||
}
|
||||
|
||||
BitcoinUnit bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
if(bitcoinUnit == null || bitcoinUnit.equals(BitcoinUnit.AUTO)) {
|
||||
bitcoinUnit = walletForm.getWallet().getAutoUnit();
|
||||
}
|
||||
|
||||
try {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
|
||||
writer.write("Date");
|
||||
writer.write("Label");
|
||||
writer.write("Value");
|
||||
writer.write("Balance");
|
||||
writer.write("Fee");
|
||||
if(fiatCurrency != null) {
|
||||
writer.write("Value (" + fiatCurrency.getCurrencyCode() + ")");
|
||||
}
|
||||
writer.write("Txid");
|
||||
writer.endRecord();
|
||||
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
TransactionEntry txEntry = (TransactionEntry)entry;
|
||||
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
|
||||
writer.write(txEntry.getLabel());
|
||||
writer.write(getCoinValue(bitcoinUnit, txEntry.getValue()));
|
||||
writer.write(getCoinValue(bitcoinUnit, txEntry.getBalance()));
|
||||
Long fee = txEntry.getValue() < 0 ? getFee(wallet, txEntry.getBlockTransaction()) : null;
|
||||
writer.write(fee == null ? "" : getCoinValue(bitcoinUnit, fee));
|
||||
if(fiatCurrency != null) {
|
||||
Double fiatValue = getFiatValue(txEntry, fiatRates);
|
||||
writer.write(fiatValue == null ? "" : getFiatValue(fiatValue));
|
||||
}
|
||||
writer.write(txEntry.getBlockTransaction().getHash().toString());
|
||||
writer.endRecord();
|
||||
}
|
||||
|
||||
if(fiatCurrency != null) {
|
||||
writer.endRecord();
|
||||
writer.writeComment(" Historical " + fiatCurrency.getCurrencyCode() + " values are taken from daily rates and should only be considered as approximate.");
|
||||
}
|
||||
|
||||
writer.close();
|
||||
} catch(IOException e) {
|
||||
throw new ExportException("Error writing transactions CSV", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Long getFee(Wallet wallet, BlockTransaction blockTransaction) {
|
||||
long fee = 0L;
|
||||
for(TransactionInput txInput : blockTransaction.getTransaction().getInputs()) {
|
||||
if(txInput.isCoinBase()) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
BlockTransaction inputTx = wallet.getWalletTransaction(txInput.getOutpoint().getHash());
|
||||
if(inputTx == null || inputTx.getTransaction().getOutputs().size() <= txInput.getOutpoint().getIndex()) {
|
||||
return null;
|
||||
}
|
||||
TransactionOutput spentOutput = inputTx.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
fee += spentOutput.getValue();
|
||||
}
|
||||
|
||||
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
|
||||
fee -= txOutput.getValue();
|
||||
}
|
||||
|
||||
return fee;
|
||||
}
|
||||
|
||||
private String getCoinValue(BitcoinUnit bitcoinUnit, Long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return BitcoinUnit.BTC.equals(bitcoinUnit) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
|
||||
}
|
||||
|
||||
private String getFiatValue(Double value) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return format.tableFormatCurrencyValue(value);
|
||||
}
|
||||
|
||||
private Double getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
|
||||
Double dayRate = null;
|
||||
if(txEntry.getBlockTransaction().getDate() == null) {
|
||||
if(AppServices.getFiatCurrencyExchangeRate() != null) {
|
||||
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
|
||||
}
|
||||
} else {
|
||||
dayRate = fiatRates.get(DateUtils.truncate(txEntry.getBlockTransaction().getDate(), Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
|
||||
if(dayRate != null) {
|
||||
return dayRate * txEntry.getValue() / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Exports a CSV file containing dates and values of the transactions in this wallet. Additionally, if a fiat currency has been configured, the fiat value on the day they were first included in the blockchain.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "csv";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
public record BBQR(BBQRType type, byte[] data) {}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class BBQRDecoder {
|
||||
private static final Logger log = LoggerFactory.getLogger(BBQRDecoder.class);
|
||||
|
||||
private final Map<Integer, byte[]> receivedParts = new TreeMap<>();
|
||||
private int totalParts;
|
||||
|
||||
private BBQRType type;
|
||||
private Result result;
|
||||
|
||||
public static boolean isBBQRFragment(String part) {
|
||||
try {
|
||||
BBQRHeader.fromString(part);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void receivePart(String part) {
|
||||
try {
|
||||
BBQRHeader header = BBQRHeader.fromString(part);
|
||||
totalParts = header.seqTotal();
|
||||
type = header.type();
|
||||
byte[] partData = header.decode(part);
|
||||
receivedParts.put(header.seqNumber(), partData);
|
||||
|
||||
if(receivedParts.size() == totalParts) {
|
||||
byte[] data = concatParts();
|
||||
|
||||
if(type == BBQRType.PSBT) {
|
||||
result = new Result(new PSBT(data));
|
||||
} else if(type == BBQRType.TXN) {
|
||||
result = new Result(new Transaction(data));
|
||||
} else if(type == BBQRType.JSON || type == BBQRType.UNICODE) {
|
||||
result = new Result(type, new String(data, StandardCharsets.UTF_8));
|
||||
} else {
|
||||
result = new Result(type, data);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not parse received QR of type " + type, e);
|
||||
result = new Result(ResultType.FAILURE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] concatParts() {
|
||||
int totalLength = 0;
|
||||
for(byte[] part : receivedParts.values()) {
|
||||
totalLength += part.length;
|
||||
}
|
||||
|
||||
byte[] data = new byte[totalLength];
|
||||
int index = 0;
|
||||
for(byte[] part : receivedParts.values()) {
|
||||
System.arraycopy(part, 0, data, index, part.length);
|
||||
index += part.length;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public int getProcessedPartsCount() {
|
||||
return receivedParts.size();
|
||||
}
|
||||
|
||||
public double getPercentComplete() {
|
||||
if(totalParts == 0) {
|
||||
return 0d;
|
||||
}
|
||||
|
||||
return (double)getProcessedPartsCount() / totalParts;
|
||||
}
|
||||
|
||||
public Result getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private final ResultType resultType;
|
||||
private final BBQRType bbqrType;
|
||||
private final PSBT psbt;
|
||||
private final Transaction transaction;
|
||||
private final String strData;
|
||||
private final byte[] data;
|
||||
private final String error;
|
||||
|
||||
public Result(ResultType resultType, String error) {
|
||||
this.resultType = resultType;
|
||||
this.bbqrType = null;
|
||||
this.psbt = null;
|
||||
this.transaction = null;
|
||||
this.strData = null;
|
||||
this.data = null;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public Result(PSBT psbt) {
|
||||
this.resultType = ResultType.SUCCESS;
|
||||
this.bbqrType = BBQRType.PSBT;
|
||||
this.psbt = psbt;
|
||||
this.transaction = null;
|
||||
this.strData = null;
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
public Result(Transaction transaction) {
|
||||
this.resultType = ResultType.SUCCESS;
|
||||
this.bbqrType = BBQRType.TXN;
|
||||
this.psbt = null;
|
||||
this.transaction = transaction;
|
||||
this.strData = null;
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
public Result(BBQRType bbqrType, String strData) {
|
||||
this.resultType = ResultType.SUCCESS;
|
||||
this.bbqrType = bbqrType;
|
||||
this.psbt = null;
|
||||
this.transaction = null;
|
||||
this.strData = strData;
|
||||
this.data = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
public Result(BBQRType bbqrType, byte[] data) {
|
||||
this.resultType = ResultType.SUCCESS;
|
||||
this.bbqrType = bbqrType;
|
||||
this.psbt = null;
|
||||
this.transaction = null;
|
||||
this.strData = null;
|
||||
this.data = data;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
public ResultType getResultType() {
|
||||
return resultType;
|
||||
}
|
||||
|
||||
public BBQRType getBbqrType() {
|
||||
return bbqrType;
|
||||
}
|
||||
|
||||
public PSBT getPsbt() {
|
||||
return psbt;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return strData;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResultType {
|
||||
SUCCESS, FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BBQREncoder {
|
||||
private final String[] parts;
|
||||
private int partIndex;
|
||||
|
||||
public BBQREncoder(BBQRType bbqrType, BBQREncoding bbqrEncoding, byte[] data, int maxFragmentLength, int firstSeqNum) {
|
||||
this.parts = encode(bbqrType, bbqrEncoding, data, maxFragmentLength).toArray(new String[0]);
|
||||
this.partIndex = firstSeqNum;
|
||||
}
|
||||
|
||||
public boolean isSinglePart() {
|
||||
return parts.length == 1;
|
||||
}
|
||||
|
||||
public String nextPart() {
|
||||
String currentPart = parts[partIndex];
|
||||
partIndex++;
|
||||
if(partIndex > parts.length - 1) {
|
||||
partIndex = 0;
|
||||
}
|
||||
|
||||
return currentPart;
|
||||
}
|
||||
|
||||
public int getNumParts() {
|
||||
return parts.length;
|
||||
}
|
||||
|
||||
private List<String> encode(BBQRType type, BBQREncoding desiredEncoding, byte[] data, int desiredChunkSize) {
|
||||
String encoded;
|
||||
BBQREncoding encoding = desiredEncoding;
|
||||
|
||||
try {
|
||||
encoded = encoding.encode(data);
|
||||
if(encoding == BBQREncoding.ZLIB) {
|
||||
String uncompressed = BBQREncoding.BASE32.encode(data);
|
||||
if(encoded.length() > uncompressed.length()) {
|
||||
throw new BBQREncodingException("Compressed data was larger than uncompressed data");
|
||||
}
|
||||
}
|
||||
} catch(BBQREncodingException e) {
|
||||
encoding = BBQREncoding.BASE32;
|
||||
encoded = BBQREncoding.BASE32.encode(data);
|
||||
}
|
||||
|
||||
int inputLength = encoded.length();
|
||||
int numChunks = (inputLength + desiredChunkSize - 1) / desiredChunkSize;
|
||||
int chunkSize = numChunks == 1 ? desiredChunkSize : (int)Math.ceil((double)inputLength / numChunks);
|
||||
|
||||
int modulo = chunkSize % encoding.getPartModulo();
|
||||
if(modulo > 0) {
|
||||
chunkSize += (encoding.getPartModulo() - modulo);
|
||||
}
|
||||
|
||||
List<String> chunks = new ArrayList<>();
|
||||
int startIndex = 0;
|
||||
for(int i = 0; i < numChunks; i ++) {
|
||||
int endIndex = Math.min(startIndex + chunkSize, encoded.length());
|
||||
BBQRHeader bbqrHeader = new BBQRHeader(encoding, type, numChunks, i);
|
||||
String chunk = bbqrHeader + encoded.substring(startIndex, endIndex);
|
||||
startIndex = endIndex;
|
||||
chunks.add(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.jcraft.jzlib.*;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum BBQREncoding {
|
||||
HEX("H") {
|
||||
@Override
|
||||
public String encode(byte[] data) throws BBQREncodingException {
|
||||
return Utils.bytesToHex(data).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decode(String part) throws BBQREncodingException {
|
||||
return Utils.hexToBytes(part);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPartModulo() {
|
||||
return 2;
|
||||
}
|
||||
}, BASE32("2") {
|
||||
@Override
|
||||
public String encode(byte[] data) throws BBQREncodingException {
|
||||
return BaseEncoding.base32().encode(data).replaceAll("=+$", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decode(String part) throws BBQREncodingException {
|
||||
return BaseEncoding.base32().decode(part);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPartModulo() {
|
||||
return 8;
|
||||
}
|
||||
}, ZLIB("Z") {
|
||||
@Override
|
||||
public String encode(byte[] data) throws BBQREncodingException {
|
||||
try {
|
||||
Deflater deflater = new Deflater(JZlib.Z_BEST_COMPRESSION, 10, true);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream zOut = new DeflaterOutputStream(out, deflater);
|
||||
zOut.write(data);
|
||||
zOut.close();
|
||||
|
||||
return BASE32.encode(out.toByteArray());
|
||||
} catch(Exception e) {
|
||||
throw new BBQREncodingException("Error deflating with zlib", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decode(String part) throws BBQREncodingException {
|
||||
try {
|
||||
Inflater inflater = new Inflater(10, true);
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(BASE32.decode(part));
|
||||
InflaterInputStream zIn = new InflaterInputStream(in, inflater);
|
||||
byte[] decoded = zIn.readAllBytes();
|
||||
zIn.close();
|
||||
|
||||
return decoded;
|
||||
} catch(Exception e) {
|
||||
throw new BBQREncodingException("Error inflating with zlib", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPartModulo() {
|
||||
return 8;
|
||||
}
|
||||
};
|
||||
|
||||
public static BBQREncoding fromString(String code) {
|
||||
for(BBQREncoding encoding : values()) {
|
||||
if(encoding.getCode().equals(code)) {
|
||||
return encoding;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Could not find encoding for code " + code);
|
||||
}
|
||||
|
||||
private final String code;
|
||||
|
||||
BBQREncoding(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public abstract String encode(byte[] data) throws BBQREncodingException;
|
||||
|
||||
public abstract byte[] decode(String part) throws BBQREncodingException;
|
||||
|
||||
public abstract int getPartModulo();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
public class BBQREncodingException extends BBQRException {
|
||||
public BBQREncodingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BBQREncodingException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
public class BBQRException extends RuntimeException {
|
||||
public BBQRException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public BBQRException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public record BBQRHeader(BBQREncoding encoding, BBQRType type, int seqTotal, int seqNumber) {
|
||||
public static final String HEADER = "B$";
|
||||
|
||||
public String toString() {
|
||||
return HEADER + encoding.getCode() + type.getCode() + encodeToBase36(seqTotal) + encodeToBase36(seqNumber);
|
||||
}
|
||||
|
||||
public byte[] decode(String part) {
|
||||
return encoding.decode(part.substring(8));
|
||||
}
|
||||
|
||||
public static BBQRHeader fromString(String part) {
|
||||
if(part.length() < 8) {
|
||||
throw new IllegalArgumentException("Part too short");
|
||||
}
|
||||
|
||||
if(!HEADER.equals(part.substring(0, 2))) {
|
||||
throw new IllegalArgumentException("Part does not start with " + HEADER);
|
||||
}
|
||||
|
||||
BBQREncoding e = BBQREncoding.fromString(part.substring(2, 3));
|
||||
BBQRType t = BBQRType.fromString(part.substring(3, 4));
|
||||
|
||||
return new BBQRHeader(e, t, decodeFromBase36(part.substring(4, 6)), decodeFromBase36(part.substring(6, 8)));
|
||||
}
|
||||
|
||||
private String encodeToBase36(int number) {
|
||||
String base36Encoded = new BigInteger(String.valueOf(number)).toString(36);
|
||||
return String.format("%2s", base36Encoded).replace(' ', '0');
|
||||
}
|
||||
|
||||
private static int decodeFromBase36(String base36) {
|
||||
BigInteger bigInteger = new BigInteger(base36, 36);
|
||||
return bigInteger.intValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.io.bbqr;
|
||||
|
||||
public enum BBQRType {
|
||||
PSBT("P"), TXN("T"), JSON("J"), CBOR("C"), UNICODE("U"), BINARY("B"), EXECUTABLE("X");
|
||||
|
||||
private final String code;
|
||||
|
||||
BBQRType(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static BBQRType fromString(String code) {
|
||||
for(BBQRType type : values()) {
|
||||
if(type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Could not find type for code " + code);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class CardStatus extends CardResponse {
|
||||
String ver;
|
||||
BigInteger birth;
|
||||
boolean tapsigner;
|
||||
boolean satschip;
|
||||
List<BigInteger> path;
|
||||
BigInteger num_backups;
|
||||
List<BigInteger> slots;
|
||||
@@ -24,7 +25,7 @@ public class CardStatus extends CardResponse {
|
||||
boolean testnet;
|
||||
|
||||
public boolean isInitialized() {
|
||||
return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
|
||||
return ((getCardType() == WalletModel.TAPSIGNER || getCardType() == WalletModel.SATSCHIP) && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
@@ -42,11 +43,11 @@ public class CardStatus extends CardResponse {
|
||||
}
|
||||
|
||||
public boolean requiresBackup() {
|
||||
return num_backups == null || num_backups.intValue() == 0;
|
||||
return !satschip && (num_backups == null || num_backups.intValue() == 0);
|
||||
}
|
||||
|
||||
public WalletModel getCardType() {
|
||||
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
|
||||
return satschip ? WalletModel.SATSCHIP : (tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD);
|
||||
}
|
||||
|
||||
public int getCurrentSlot() {
|
||||
@@ -72,6 +73,7 @@ public class CardStatus extends CardResponse {
|
||||
", ver='" + ver + '\'' +
|
||||
", birth=" + birth +
|
||||
", tapsigner=" + tapsigner +
|
||||
", satschip=" + satschip +
|
||||
", path=" + path +
|
||||
", num_backups=" + num_backups +
|
||||
", slots=" + slots +
|
||||
|
||||
@@ -32,10 +32,6 @@ public class CkCardApi extends CardApi {
|
||||
private final CardProtocol cardProtocol;
|
||||
private String cvc;
|
||||
|
||||
public CkCardApi(String cvc) throws CardException {
|
||||
this(WalletModel.TAPSIGNER, cvc);
|
||||
}
|
||||
|
||||
public CkCardApi(WalletModel cardType, String cvc) throws CardException {
|
||||
this.cardType = cardType;
|
||||
this.cardProtocol = new CardProtocol();
|
||||
@@ -137,6 +133,8 @@ public class CkCardApi extends CardApi {
|
||||
public Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty) {
|
||||
if(cardType == WalletModel.TAPSIGNER) {
|
||||
return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty);
|
||||
} else if(cardType == WalletModel.SATSCHIP) {
|
||||
return new CardImportPane.CardInitializationService(new Satschip(), cvc, entropy, messageProperty);
|
||||
}
|
||||
|
||||
return new CardInitializationService(entropy, messageProperty);
|
||||
@@ -144,6 +142,10 @@ public class CkCardApi extends CardApi {
|
||||
|
||||
@Override
|
||||
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
if(cardType == WalletModel.SATSCHIP) {
|
||||
return new CardImportPane.CardImportService(new Satschip(), cvc, derivation, messageProperty);
|
||||
}
|
||||
|
||||
return new CardImportPane.CardImportService(new Tapsigner(), cvc, derivation, messageProperty);
|
||||
}
|
||||
|
||||
@@ -155,11 +157,11 @@ public class CkCardApi extends CardApi {
|
||||
ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub));
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.TAPSIGNER.toDisplayString());
|
||||
keystore.setLabel(cardType.toDisplayString());
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setExtendedPublicKey(derivedXpubkey);
|
||||
keystore.setWalletModel(WalletModel.TAPSIGNER);
|
||||
keystore.setWalletModel(cardType);
|
||||
|
||||
return keystore;
|
||||
}
|
||||
@@ -404,7 +406,7 @@ public class CkCardApi extends CardApi {
|
||||
@Override
|
||||
protected PSBT call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER && cardStatus.getCardType() != WalletModel.SATSCHIP) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign transactions.");
|
||||
}
|
||||
|
||||
@@ -465,7 +467,7 @@ public class CkCardApi extends CardApi {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER && cardStatus.getCardType() != WalletModel.SATSCHIP) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign messages.");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public class Satschip extends Tapsigner {
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the keystore from your Satschip by placing it on the card reader.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Satschip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SATSCHIP;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class Tapsigner implements KeystoreCardImport {
|
||||
public boolean isInitialized() throws CardException {
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(null);
|
||||
cardApi = new CkCardApi(getWalletModel(), null);
|
||||
return cardApi.isInitialized();
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
@@ -37,7 +37,7 @@ public class Tapsigner implements KeystoreCardImport {
|
||||
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(pin);
|
||||
cardApi = new CkCardApi(getWalletModel(), pin);
|
||||
CardStatus cardStatus = cardApi.getStatus();
|
||||
if(cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Card is already initialized.");
|
||||
@@ -63,7 +63,7 @@ public class Tapsigner implements KeystoreCardImport {
|
||||
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(pin);
|
||||
cardApi = new CkCardApi(getWalletModel(), pin);
|
||||
CardStatus cardStatus = cardApi.getStatus();
|
||||
if(!cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Card is not initialized.");
|
||||
|
||||
@@ -192,7 +192,7 @@ public class SatoCardApi extends CardApi {
|
||||
String fullpath = KeyDerivation.writePath(derivation);
|
||||
cardProtocol.cardVerifyPIN(0, pin);
|
||||
|
||||
// 2FA is optionnal, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
|
||||
// 2FA is optional, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
|
||||
SatoCardStatus cardStatus = this.getStatus();
|
||||
if(cardStatus.needs2FA()) {
|
||||
throw new CardException("Satochip 2FA is not (yet) supported within Sparrow");
|
||||
|
||||
@@ -46,7 +46,7 @@ public class SatochipCommandSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
|
||||
* Returns the application info as stored from the last sent SELECT command. Returns null if no successful SELECT
|
||||
* command has been sent using this command set.
|
||||
*
|
||||
* @return the application info object
|
||||
@@ -347,8 +347,14 @@ public class SatochipCommandSet {
|
||||
public APDUResponse cardBip32GetExtendedKey(byte[] bytePath) {
|
||||
byte p1 = (byte) (bytePath.length / 4);
|
||||
|
||||
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
|
||||
return this.cardTransmit(plainApdu);
|
||||
APDUCommand capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
|
||||
APDUResponse rapdu = this.cardTransmit(capdu);
|
||||
if(rapdu.getSw() == 0x9C01) {
|
||||
// error 0X9C01: no memory available for key derivation => flush memory cache in card
|
||||
capdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x80, bytePath);
|
||||
rapdu = this.cardTransmit(capdu);
|
||||
}
|
||||
return rapdu;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.control.CardImportPane;
|
||||
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.Satschip;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
|
||||
import com.sparrowwallet.sparrow.io.satochip.Satochip;
|
||||
import javafx.fxml.FXML;
|
||||
@@ -25,7 +26,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> fileImporters = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
|
||||
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux(), new AirGapVault());
|
||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY(), new Krux());
|
||||
}
|
||||
@@ -39,7 +40,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip());
|
||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip(), new Satschip());
|
||||
for(KeystoreCardImport importer : cardImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
|
||||
@@ -55,6 +55,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
dialogPane.setPrefWidth(650);
|
||||
dialogPane.setPrefHeight(690);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? getWatchOnlyKeystore() : null);
|
||||
|
||||
@@ -228,7 +228,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates: " + e.getErrors(), e);
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates from connected server: " + e.getErrors(), e);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Error getting fee estimates for target blocks: " + targetBlocks, e);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
|
||||
@@ -30,7 +28,7 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET, Network.TESTNET);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
@@ -51,7 +49,7 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
@@ -74,7 +72,7 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
@@ -95,7 +93,7 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(Proxy proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
@@ -131,7 +129,7 @@ public enum BroadcastSource {
|
||||
return onionUrl;
|
||||
}
|
||||
|
||||
public String getBaseUrl(Proxy proxy) {
|
||||
public String getBaseUrl(HostAndPort proxy) {
|
||||
return (proxy == null ? getTlsUrl() : getOnionUrl());
|
||||
}
|
||||
|
||||
@@ -139,48 +137,30 @@ public enum BroadcastSource {
|
||||
|
||||
public abstract List<Network> getSupportedNetworks();
|
||||
|
||||
protected abstract URL getURL(Proxy proxy) throws MalformedURLException;
|
||||
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException;
|
||||
|
||||
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
|
||||
Proxy proxy = AppServices.getProxy(Integer.toString(secureRandom.nextInt()));
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
httpClientService.changeIdentity();
|
||||
|
||||
try {
|
||||
URL url = getURL(proxy);
|
||||
URL url = getURL(httpClientService.getTorProxy());
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Broadcasting transaction to " + url);
|
||||
}
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection)url.openConnection() : (HttpURLConnection)url.openConnection(proxy);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "text/plain");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
try(OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream())) {
|
||||
writer.write(data);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String responseLine;
|
||||
while((responseLine = br.readLine()) != null) {
|
||||
response.append(responseLine.trim());
|
||||
}
|
||||
}
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if(statusCode < 200 || statusCode >= 300) {
|
||||
throw new BroadcastException("Could not broadcast transaction, server returned " + statusCode + ": " + response);
|
||||
}
|
||||
String response = httpClientService.postString(url.toString(), null, "text/plain", data);
|
||||
|
||||
try {
|
||||
return Sha256Hash.wrap(response.toString().trim());
|
||||
return Sha256Hash.wrap(response.trim());
|
||||
} catch(Exception e) {
|
||||
throw new BroadcastException("Could not retrieve txid from broadcast, server returned " + statusCode + ": " + response);
|
||||
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
} catch(JavaHttpException 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);
|
||||
throw new BroadcastException("Could not broadcast transaction via " + getName(), e);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.CormorantBitcoindException;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
@@ -503,19 +504,19 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
|
||||
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd) throws ServerException {
|
||||
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes) throws ServerException {
|
||||
try {
|
||||
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
|
||||
for(int i = indexStart; i < transaction.getOutputs().size() && i < indexEnd; i++) {
|
||||
TransactionOutput output = transaction.getOutputs().get(i);
|
||||
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
|
||||
if(blockTransactionHashes.get(i) == null) {
|
||||
TransactionOutput output = transaction.getOutputs().get(i);
|
||||
pathScriptHashes.put(Integer.toString(i), getScriptHash(output));
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
|
||||
|
||||
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
blockTransactionHashes.add(null);
|
||||
Map<String, ScriptHashTx[]> result = new HashMap<>();
|
||||
if(!pathScriptHashes.isEmpty()) {
|
||||
result = electrumServerRpc.getScriptHashHistory(getTransport(), null, pathScriptHashes, false);
|
||||
}
|
||||
|
||||
for(String index : result.keySet()) {
|
||||
@@ -812,6 +813,18 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) 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)) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
}
|
||||
|
||||
return targetBlocksFeeRatesSats;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||
try {
|
||||
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
|
||||
|
||||
@@ -824,15 +837,12 @@ public class ElectrumServer {
|
||||
targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d);
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(Network.get().equals(Network.MAINNET)) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
}
|
||||
|
||||
return targetBlocksFeeRatesSats;
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
throw new ServerException(e.getMessage(), e);
|
||||
log.warn(e.getMessage());
|
||||
return targetBlocks.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> AppServices.getFallbackFeeRate(),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,17 +1571,26 @@ public class ElectrumServer {
|
||||
private final Transaction transaction;
|
||||
private final int indexStart;
|
||||
private final int indexEnd;
|
||||
|
||||
public TransactionOutputsReferenceService(Transaction transaction) {
|
||||
this.transaction = transaction;
|
||||
this.indexStart = 0;
|
||||
this.indexEnd = transaction.getOutputs().size();
|
||||
}
|
||||
private final List<Set<BlockTransactionHash>> blockTransactionHashes;
|
||||
private final Map<Sha256Hash, BlockTransaction> transactionMap;
|
||||
|
||||
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd) {
|
||||
this.transaction = transaction;
|
||||
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
|
||||
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
|
||||
this.blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
blockTransactionHashes.add(null);
|
||||
}
|
||||
this.transactionMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public TransactionOutputsReferenceService(Transaction transaction, int indexStart, int indexEnd, List<Set<BlockTransactionHash>> blockTransactionHashes, Map<Sha256Hash, BlockTransaction> transactionMap) {
|
||||
this.transaction = transaction;
|
||||
this.indexStart = Math.min(transaction.getOutputs().size(), indexStart);
|
||||
this.indexEnd = Math.min(transaction.getOutputs().size(), indexEnd);
|
||||
this.blockTransactionHashes = blockTransactionHashes;
|
||||
this.transactionMap = transactionMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1579,7 +1598,7 @@ public class ElectrumServer {
|
||||
return new Task<>() {
|
||||
protected List<BlockTransaction> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd);
|
||||
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction, indexStart, indexEnd, blockTransactionHashes);
|
||||
|
||||
Set<BlockTransactionHash> setReferences = new HashSet<>();
|
||||
for(Set<BlockTransactionHash> outputReferences : outputTransactionReferences) {
|
||||
@@ -1589,16 +1608,16 @@ public class ElectrumServer {
|
||||
}
|
||||
setReferences.remove(null);
|
||||
setReferences.remove(UNFETCHABLE_BLOCK_TRANSACTION);
|
||||
setReferences.removeIf(ref -> transactionMap.get(ref.getHash()) != null);
|
||||
|
||||
List<BlockTransaction> blockTransactions = new ArrayList<>(transaction.getOutputs().size());
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
blockTransactions.add(null);
|
||||
}
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
||||
if(!setReferences.isEmpty()) {
|
||||
Map<Integer, BlockHeader> blockHeaderMap = electrumServer.getBlockHeaders(null, setReferences);
|
||||
transactionMap = electrumServer.getTransactions(null, setReferences, blockHeaderMap);
|
||||
transactionMap.putAll(electrumServer.getTransactions(null, setReferences, blockHeaderMap));
|
||||
}
|
||||
|
||||
for(int i = 0; i < outputTransactionReferences.size(); i++) {
|
||||
@@ -1683,7 +1702,7 @@ public class ElectrumServer {
|
||||
|
||||
for(int i = 0; i < wallets.size(); i++) {
|
||||
Wallet wallet = wallets.get(i);
|
||||
updateProgress(i, wallets.size() + StandardAccount.values().length);
|
||||
updateProgress(i, wallets.size() + StandardAccount.DISCOVERY_ACCOUNTS.size());
|
||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
||||
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
|
||||
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
@@ -1696,13 +1715,13 @@ public class ElectrumServer {
|
||||
Map<WalletNode, Set<BlockTransactionHash>> childTransactionMap = new TreeMap<>();
|
||||
electrumServer.getReferences(childWallet, childWallet.getNode(KeyPurpose.RECEIVE).getChildren(), childTransactionMap, 0);
|
||||
if(childTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
if(StandardAccount.isWhirlpoolAccount(standardAccount)) {
|
||||
foundAccounts.addAll(StandardAccount.WHIRLPOOL_ACCOUNTS);
|
||||
} else {
|
||||
foundAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
updateProgress(i + j, wallets.size() + StandardAccount.values().length);
|
||||
updateProgress(i + j, wallets.size() + StandardAccount.DISCOVERY_ACCOUNTS.size());
|
||||
}
|
||||
|
||||
for(StandardAccount standardAccount : foundAccounts) {
|
||||
@@ -1724,8 +1743,8 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
List<StandardAccount> accounts = new ArrayList<>();
|
||||
for(StandardAccount account : StandardAccount.values()) {
|
||||
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
|
||||
for(StandardAccount account : StandardAccount.DISCOVERY_ACCOUNTS) {
|
||||
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.isWhirlpoolAccount(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
|
||||
accounts.add(account);
|
||||
}
|
||||
}
|
||||
@@ -1882,7 +1901,7 @@ public class ElectrumServer {
|
||||
|
||||
private PayNym getPayNym(PaymentCode paymentCode) {
|
||||
try {
|
||||
return AppServices.getPayNymService().getPayNym(paymentCode.toString()).blockingFirst();
|
||||
return PayNymService.getPayNym(paymentCode.toString()).blockingFirst();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -29,6 +29,11 @@ public enum ExchangeSource {
|
||||
public Double getExchangeRate(Currency currency) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
},
|
||||
COINBASE("Coinbase") {
|
||||
@Override
|
||||
@@ -50,15 +55,14 @@ public enum ExchangeSource {
|
||||
|
||||
private CoinbaseRates getRates() {
|
||||
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting exchange rates from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinbaseRates.class);
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
return httpClientService.requestJson(url, CoinbaseRates.class, null);
|
||||
} catch (Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving currency rates", e);
|
||||
@@ -68,6 +72,52 @@ public enum ExchangeSource {
|
||||
return new CoinbaseRates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
|
||||
Map<Date, Double> historicalRates = new TreeMap<>();
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
Instant currentInstant = start.toInstant();
|
||||
Instant endInstant = end.toInstant();
|
||||
|
||||
while(currentInstant.isBefore(endInstant) || currentInstant.equals(endInstant)) {
|
||||
Date fromDate = Date.from(currentInstant.atZone(ZoneId.systemDefault()).toInstant());
|
||||
currentInstant = currentInstant.plus(300, ChronoUnit.DAYS);
|
||||
Date toDate = Date.from(currentInstant.atZone(ZoneId.systemDefault()).toInstant());
|
||||
toDate = toDate.after(end) ? end : toDate;
|
||||
|
||||
String startTime = dateFormat.format(fromDate);
|
||||
String endTime = dateFormat.format(toDate);
|
||||
|
||||
String url = "https://api.pro.coinbase.com/products/BTC-" + currency.getCurrencyCode() + "/candles?start=" + startTime + "T12:00:00&end=" + endTime + "T12:00:00&granularity=86400";
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting historical exchange rates from " + url);
|
||||
}
|
||||
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
Number[][] coinbaseData = httpClientService.requestJson(url, Number[][].class, Map.of("User-Agent", "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)", "Accept", "*/*"));
|
||||
for(Number[] price : coinbaseData) {
|
||||
Date date = new Date(price[0].longValue() * 1000);
|
||||
historicalRates.put(DateUtils.truncate(date, Calendar.DAY_OF_MONTH), price[4].doubleValue());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving historical currency rates", e);
|
||||
} else {
|
||||
if(e instanceof JavaHttpException javaHttpException && javaHttpException.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() + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return historicalRates;
|
||||
}
|
||||
},
|
||||
COINGECKO("Coingecko") {
|
||||
@Override
|
||||
@@ -89,15 +139,14 @@ public enum ExchangeSource {
|
||||
|
||||
private CoinGeckoRates getRates() {
|
||||
String url = "https://api.coingecko.com/api/v3/exchange_rates";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting exchange rates from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinGeckoRates.class);
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
return httpClientService.requestJson(url, CoinGeckoRates.class, null);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving currency rates", e);
|
||||
@@ -107,6 +156,36 @@ public enum ExchangeSource {
|
||||
return new CoinGeckoRates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end) {
|
||||
long startDate = start.getTime() / 1000;
|
||||
long endDate = end.getTime() / 1000;
|
||||
|
||||
String url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range?vs_currency=" + currency.getCurrencyCode() + "&from=" + startDate + "&to=" + endDate;
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting historical exchange rates from " + url);
|
||||
}
|
||||
|
||||
Map<Date, Double> historicalRates = new TreeMap<>();
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
CoinGeckoHistoricalRates coinGeckoHistoricalRates = httpClientService.requestJson(url, CoinGeckoHistoricalRates.class, null);
|
||||
for(List<Number> historicalRate : coinGeckoHistoricalRates.prices) {
|
||||
Date date = new Date(historicalRate.get(0).longValue());
|
||||
historicalRates.put(DateUtils.truncate(date, Calendar.DAY_OF_MONTH), historicalRate.get(1).doubleValue());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving historical currency rates", e);
|
||||
} else {
|
||||
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return historicalRates;
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExchangeSource.class);
|
||||
@@ -121,6 +200,8 @@ public enum ExchangeSource {
|
||||
|
||||
public abstract Double getExchangeRate(Currency currency);
|
||||
|
||||
public abstract Map<Date, Double> getHistoricalExchangeRates(Currency currency, Date start, Date end);
|
||||
|
||||
private static boolean isValidISO4217Code(String code) {
|
||||
try {
|
||||
Currency currency = Currency.getInstance(code);
|
||||
@@ -176,22 +257,26 @@ public enum ExchangeSource {
|
||||
}
|
||||
|
||||
private static class CoinbaseRates {
|
||||
CoinbaseData data;
|
||||
public CoinbaseData data = new CoinbaseData();
|
||||
}
|
||||
|
||||
private static class CoinbaseData {
|
||||
String currency;
|
||||
Map<String, Double> rates;
|
||||
public String currency;
|
||||
public Map<String, Double> rates = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private static class CoinGeckoRates {
|
||||
Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
|
||||
public Map<String, CoinGeckoRate> rates = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private static class CoinGeckoRate {
|
||||
String name;
|
||||
String unit;
|
||||
Double value;
|
||||
String type;
|
||||
public String name;
|
||||
public String unit;
|
||||
public Double value;
|
||||
public String type;
|
||||
}
|
||||
|
||||
private static class CoinGeckoHistoricalRates {
|
||||
public List<List<Number>> prices = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -26,14 +19,14 @@ public enum FeeRatesSource {
|
||||
@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(defaultblockTargetFeeRates, url);
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
MINIMUM("Minimum (1 sat/vB)") {
|
||||
@@ -46,6 +39,22 @@ public enum FeeRatesSource {
|
||||
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
},
|
||||
OXT_ME("oxt.me") {
|
||||
@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";
|
||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreeTierRates getThreeTierRates(String url, HttpClientService httpClientService) throws Exception {
|
||||
OxtRates oxtRates = httpClientService.requestJson(url, OxtRates.class, null);
|
||||
if(oxtRates.data == null || oxtRates.data.length < 1) {
|
||||
throw new Exception("Invalid response from " + url);
|
||||
}
|
||||
return oxtRates.data[0].getThreeTierRates();
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
|
||||
@@ -65,17 +74,15 @@ public enum FeeRatesSource {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting fee rates from " + url);
|
||||
}
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
ThreeTierRates threeTierRates = feeRatesSource.getThreeTierRates(url, httpClientService);
|
||||
Double lastRate = null;
|
||||
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
|
||||
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
|
||||
@@ -110,15 +117,22 @@ public enum FeeRatesSource {
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
|
||||
protected ThreeTierRates getThreeTierRates(String url, HttpClientService httpClientService) throws Exception {
|
||||
return httpClientService.requestJson(url, ThreeTierRates.class, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static class ThreeTierRates {
|
||||
Double fastestFee;
|
||||
Double halfHourFee;
|
||||
Double hourFee;
|
||||
Double minimumFee;
|
||||
private record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
|
||||
|
||||
private record OxtRates(OxtRatesData[] data) {}
|
||||
|
||||
private record OxtRatesData(Double recommended_fee_099, Double recommended_fee_090, Double recommended_fee_050) {
|
||||
public ThreeTierRates getThreeTierRates() {
|
||||
return new ThreeTierRates(recommended_fee_099/1000, recommended_fee_090/1000, recommended_fee_050/1000, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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 io.reactivex.Observable;
|
||||
import java8.util.Optional;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpClientService {
|
||||
private final JavaHttpClientService httpClientService;
|
||||
|
||||
public HttpClientService(HostAndPort torProxy) {
|
||||
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
httpClientService.shutdown();
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
private final HttpClientService httpClientService;
|
||||
|
||||
public ShutdownService(HttpClientService httpClientService) {
|
||||
this.httpClientService = httpClientService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
httpClientService.shutdown();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,15 @@ public enum PublicElectrumServer {
|
||||
DIYNODES_COM("electrum.diynodes.com", "ssl://electrum.diynodes.com:50022", Network.MAINNET),
|
||||
SETHFORPRIVACY_COM("fulcrum.sethforprivacy.com", "ssl://fulcrum.sethforprivacy.com:50002", Network.MAINNET),
|
||||
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET),
|
||||
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET);
|
||||
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET),
|
||||
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET);
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network) {
|
||||
this.server = new Server(url, name);
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
|
||||
|
||||
private final Server server;
|
||||
private final Network network;
|
||||
|
||||
@@ -117,10 +117,11 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
}
|
||||
|
||||
protected boolean shouldSaveCertificate() {
|
||||
//Avoid saving the certificates for blockstream.info public servers - they change too often and encourage approval complacency
|
||||
if(PublicElectrumServer.BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())
|
||||
|| PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())) {
|
||||
return false;
|
||||
//Avoid saving the certificates for public servers - they change often, encourage approval complacency, and there is little a user can do to check
|
||||
for(PublicElectrumServer publicElectrumServer : PublicElectrumServer.getServers()) {
|
||||
if(publicElectrumServer.getServer().getHost().equals(server.getHost())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return Storage.getCertificateFile(server.getHost()) == null;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
public class TorUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorUtils.class);
|
||||
|
||||
public static void changeIdentity(HostAndPort proxy) {
|
||||
if(AppServices.isTorRunning()) {
|
||||
Tor.getDefault().getTorManager().signal(TorControlSignal.Signal.NewNym, throwable -> {
|
||||
log.warn("Failed to signal newnym");
|
||||
}, successEvent -> {
|
||||
log.info("Signalled newnym for new Tor circuit");
|
||||
});
|
||||
} else {
|
||||
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
||||
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
||||
writeNewNym(socket);
|
||||
} catch(Exception e) {
|
||||
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeNewNym(Socket socket) throws IOException {
|
||||
log.debug("Sending NEWNYM to " + socket);
|
||||
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
|
||||
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
@@ -13,12 +12,7 @@ import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -26,12 +20,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
|
||||
private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class);
|
||||
private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version";
|
||||
|
||||
private static String version;
|
||||
|
||||
@Override
|
||||
protected Task<VersionUpdatedEvent> createTask() {
|
||||
return new Task<>() {
|
||||
protected VersionUpdatedEvent call() {
|
||||
try {
|
||||
VersionCheck versionCheck = getVersionCheck();
|
||||
version = versionCheck.version;
|
||||
if(isNewer(versionCheck) && verifySignature(versionCheck)) {
|
||||
return new VersionUpdatedEvent(versionCheck.version);
|
||||
}
|
||||
@@ -45,18 +42,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
|
||||
}
|
||||
|
||||
private VersionCheck getVersionCheck() throws IOException {
|
||||
URL url = new URL(VERSION_CHECK_URL);
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting application version check from " + url);
|
||||
log.info("Requesting application version check from " + VERSION_CHECK_URL);
|
||||
}
|
||||
|
||||
HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy));
|
||||
|
||||
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, VersionCheck.class);
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
return httpClientService.requestJson(VERSION_CHECK_URL, VersionCheck.class, null);
|
||||
} catch(Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +94,12 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
private static class VersionCheck {
|
||||
public String version;
|
||||
public Map<String, String> signatures;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ public class Cormorant {
|
||||
}
|
||||
|
||||
public boolean checkWalletImport(Wallet wallet) {
|
||||
if(bitcoindClient == null) {
|
||||
log.warn("Attempting to check if " + wallet.getMasterName() + " is imported, but Cormorant is not started");
|
||||
return false;
|
||||
}
|
||||
|
||||
//Will block until all wallet descriptors have been added
|
||||
try {
|
||||
bitcoindClient.importWallet(wallet);
|
||||
@@ -73,6 +78,11 @@ public class Cormorant {
|
||||
}
|
||||
|
||||
public void checkAddressImport(Address address, Date since) throws ServerException {
|
||||
if(bitcoindClient == null) {
|
||||
log.warn("Attempting to check if an address is imported, but Cormorant is not started");
|
||||
throw new ServerException("Server is not connected");
|
||||
}
|
||||
|
||||
//Will block until address descriptor has been added
|
||||
try {
|
||||
bitcoindClient.importAddress(address, since);
|
||||
@@ -86,7 +96,9 @@ public class Cormorant {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
bitcoindClient.stop();
|
||||
if(bitcoindClient != null) {
|
||||
bitcoindClient.stop();
|
||||
}
|
||||
if(electrumServer != null) {
|
||||
electrumServer.stop();
|
||||
}
|
||||
|
||||
+32
-14
@@ -46,6 +46,8 @@ public class BitcoindClient {
|
||||
private static final int DEFAULT_GAP_LIMIT = 1000;
|
||||
private static final int POSTMIX_GAP_LIMIT = 4000;
|
||||
|
||||
private static final long PRUNED_RESCAN_TIMEGAP_MILLIS = 7200*1000;
|
||||
|
||||
private final JsonRpcClient jsonRpcClient;
|
||||
private final Timer timer = new Timer(true);
|
||||
private final Store store = new Store();
|
||||
@@ -58,6 +60,7 @@ public class BitcoindClient {
|
||||
private final Map<String, ScanDate> importedDescriptors = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private final Map<String, Date> descriptorBirthDates = new HashMap<>();
|
||||
private final Map<String, Integer> descriptorUsedIndexes = new HashMap<>();
|
||||
|
||||
private boolean initialized;
|
||||
private boolean stopped;
|
||||
@@ -257,7 +260,7 @@ public class BitcoindClient {
|
||||
}
|
||||
|
||||
private ScanDate getScanDate(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose, Date earliestBirthDate) {
|
||||
Integer range = (keyPurpose == null ? null : wallet.getFreshNode(keyPurpose).getIndex() + getGapLimit(wallet, keyPurpose));
|
||||
Integer range = (keyPurpose == null ? null : Math.max(getWalletRange(normalizedDescriptor, wallet, keyPurpose), getDefaultRange(wallet, keyPurpose)));
|
||||
|
||||
//Force a rescan if loading a wallet with a birthday later than existing transactions, or if the wallet birthdate has been set or changed to an earlier date from the last check
|
||||
boolean forceRescan = false;
|
||||
@@ -271,7 +274,19 @@ public class BitcoindClient {
|
||||
return new ScanDate(earliestBirthDate, range, forceRescan);
|
||||
}
|
||||
|
||||
private int getGapLimit(Wallet wallet, KeyPurpose keyPurpose) {
|
||||
private int getWalletRange(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose) {
|
||||
int maxIndex = getHighestUsedIndex(normalizedDescriptor, wallet, keyPurpose) + wallet.getGapLimit();
|
||||
//Default keypool size of 1000 will mean no rescans with a normal (<1000) gap limit as range is automatically extended by keypool size on address use
|
||||
//Rounding up to the nearest 500 will ensure reduced rescans where gap limit is over 1000
|
||||
return maxIndex % 500 == 0 ? maxIndex : ((maxIndex / 500) + 1) * 500;
|
||||
}
|
||||
|
||||
private int getHighestUsedIndex(String descriptor, Wallet wallet, KeyPurpose keyPurpose) {
|
||||
int highestUsedIndex = wallet.getFreshNode(keyPurpose).getIndex() - 1;
|
||||
return descriptorUsedIndexes.compute(descriptor, (d, lastHighestUsedIndex) -> lastHighestUsedIndex == null ? highestUsedIndex : Math.max(highestUsedIndex, lastHighestUsedIndex));
|
||||
}
|
||||
|
||||
private int getDefaultRange(Wallet wallet, KeyPurpose keyPurpose) {
|
||||
return wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && keyPurpose == KeyPurpose.RECEIVE ? POSTMIX_GAP_LIMIT : DEFAULT_GAP_LIMIT;
|
||||
}
|
||||
|
||||
@@ -314,6 +329,7 @@ public class BitcoindClient {
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Date> optPrunedDate = pruned ? getPrunedDate() : Optional.empty();
|
||||
Map<String, ScanDate> importingDescriptors = new LinkedHashMap<>(descriptors);
|
||||
importingDescriptors.keySet().removeAll(importedDescriptors.keySet());
|
||||
for(Map.Entry<String, ScanDate> entry : descriptors.entrySet()) {
|
||||
@@ -322,23 +338,25 @@ public class BitcoindClient {
|
||||
}
|
||||
|
||||
ScanDate scanDate = entry.getValue();
|
||||
if(scanDate.forceRescan) {
|
||||
ScanDate importedScanDate = importedDescriptors.get(entry.getKey());
|
||||
ScanDate importedScanDate = importedDescriptors.get(entry.getKey());
|
||||
if(scanDate.range != null && importedScanDate != null && importedScanDate.range != null && scanDate.range > importedScanDate.range) {
|
||||
Date rescanSince = scanDate.rescanSince != null && (importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince)) ? scanDate.rescanSince : importedScanDate.rescanSince;
|
||||
Optional<Date> optPrunedStart = optPrunedDate.map(date -> new Date(date.getTime() + PRUNED_RESCAN_TIMEGAP_MILLIS));
|
||||
rescanSince = optPrunedStart.isPresent() && (rescanSince == null || optPrunedStart.get().after(rescanSince)) ? optPrunedStart.get() : rescanSince;
|
||||
importingDescriptors.put(entry.getKey(), new ScanDate(rescanSince, scanDate.range, false));
|
||||
} else if(scanDate.forceRescan) {
|
||||
if(scanDate.rescanSince != null && (importedScanDate == null || importedScanDate.rescanSince == null || scanDate.rescanSince.before(importedScanDate.rescanSince))) {
|
||||
importingDescriptors.put(entry.getKey(), new ScanDate(scanDate.rescanSince, importedScanDate != null ? importedScanDate.range : scanDate.range, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(pruned) {
|
||||
Optional<Date> optPrunedDate = getPrunedDate();
|
||||
if(optPrunedDate.isPresent()) {
|
||||
Date prunedDate = optPrunedDate.get();
|
||||
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
|
||||
if(optPrePruneImport.isPresent()) {
|
||||
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
|
||||
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
|
||||
}
|
||||
if(optPrunedDate.isPresent()) {
|
||||
Date prunedDate = optPrunedDate.get();
|
||||
Optional<Map.Entry<String, ScanDate>> optPrePruneImport = importingDescriptors.entrySet().stream().filter(entry -> entry.getValue().rescanSince != null && entry.getValue().rescanSince.before(prunedDate)).findFirst();
|
||||
if(optPrePruneImport.isPresent()) {
|
||||
Map.Entry<String, ScanDate> prePruneImport = optPrePruneImport.get();
|
||||
throw new ScanDateBeforePruneException(prePruneImport.getKey(), prePruneImport.getValue().rescanSince, prunedDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +433,7 @@ public class BitcoindClient {
|
||||
}
|
||||
|
||||
try {
|
||||
if(listTransaction.category() == Category.receive) {
|
||||
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
|
||||
//Transactions received to an address can be added directly
|
||||
Address address = Address.fromString(listTransaction.address());
|
||||
String updatedScriptHash = store.addAddressTransaction(address, listTransaction);
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Store {
|
||||
private final Map<String, MempoolEntry> mempoolEntries = new HashMap<>();
|
||||
|
||||
public String addAddressTransaction(Address address, ListTransaction listTransaction) {
|
||||
if(listTransaction.category() == Category.receive) {
|
||||
if(listTransaction.category() == Category.receive || listTransaction.category() == Category.immature || listTransaction.category() == Category.generate) {
|
||||
fundingAddresses.put(new HashIndex(Sha256Hash.wrap(listTransaction.txid()), listTransaction.vout()), address);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user